viberadar 0.3.44 → 0.3.46
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/dist/scanner/index.d.ts +0 -2
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +0 -1
- package/dist/scanner/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +564 -486
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +410 -687
- package/package.json +43 -43
package/dist/ui/dashboard.html
CHANGED
|
@@ -83,68 +83,6 @@
|
|
|
83
83
|
#termBtn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--dim); }
|
|
84
84
|
#termBtn.term-active { color: var(--accent); border-color: var(--accent); }
|
|
85
85
|
|
|
86
|
-
#agentBtn {
|
|
87
|
-
padding: 5px 12px;
|
|
88
|
-
background: var(--bg);
|
|
89
|
-
border: 1px solid var(--border);
|
|
90
|
-
border-radius: 6px;
|
|
91
|
-
color: var(--muted);
|
|
92
|
-
font-size: 12px;
|
|
93
|
-
cursor: pointer;
|
|
94
|
-
display: flex;
|
|
95
|
-
align-items: center;
|
|
96
|
-
gap: 5px;
|
|
97
|
-
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
98
|
-
white-space: nowrap;
|
|
99
|
-
}
|
|
100
|
-
#agentBtn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--dim); }
|
|
101
|
-
|
|
102
|
-
.agent-menu {
|
|
103
|
-
display: none;
|
|
104
|
-
position: absolute;
|
|
105
|
-
top: calc(100% + 6px);
|
|
106
|
-
right: 0;
|
|
107
|
-
min-width: 200px;
|
|
108
|
-
background: var(--bg-card);
|
|
109
|
-
border: 1px solid var(--border);
|
|
110
|
-
border-radius: 8px;
|
|
111
|
-
padding: 6px;
|
|
112
|
-
z-index: 1000;
|
|
113
|
-
box-shadow: 0 8px 24px rgba(0,0,0,0.4);
|
|
114
|
-
}
|
|
115
|
-
.agent-menu.open { display: block; }
|
|
116
|
-
.agent-menu-label {
|
|
117
|
-
font-size: 10px;
|
|
118
|
-
text-transform: uppercase;
|
|
119
|
-
letter-spacing: 0.5px;
|
|
120
|
-
color: var(--dim);
|
|
121
|
-
padding: 6px 10px 4px;
|
|
122
|
-
}
|
|
123
|
-
.agent-menu-item {
|
|
124
|
-
display: flex;
|
|
125
|
-
align-items: center;
|
|
126
|
-
justify-content: space-between;
|
|
127
|
-
gap: 8px;
|
|
128
|
-
width: 100%;
|
|
129
|
-
padding: 8px 10px;
|
|
130
|
-
background: none;
|
|
131
|
-
border: none;
|
|
132
|
-
border-radius: 6px;
|
|
133
|
-
color: var(--text);
|
|
134
|
-
font-size: 13px;
|
|
135
|
-
cursor: pointer;
|
|
136
|
-
text-align: left;
|
|
137
|
-
transition: background 0.1s;
|
|
138
|
-
}
|
|
139
|
-
.agent-menu-item:hover { background: var(--bg-hover); }
|
|
140
|
-
.agent-menu-check { color: var(--green); font-weight: 700; display: none; }
|
|
141
|
-
.agent-menu-check.active { display: inline; }
|
|
142
|
-
.agent-menu-divider {
|
|
143
|
-
height: 1px;
|
|
144
|
-
background: var(--border);
|
|
145
|
-
margin: 4px 0;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
86
|
/* ── Stats bar ───────────────────────────────────────────────────────────── */
|
|
149
87
|
.stats-bar {
|
|
150
88
|
display: flex;
|
|
@@ -234,8 +172,7 @@
|
|
|
234
172
|
.type-count { margin-left: auto; font-size: 11px; color: var(--dim); }
|
|
235
173
|
|
|
236
174
|
/* ── Content area ────────────────────────────────────────────────────────── */
|
|
237
|
-
.content { flex: 1; overflow-y: auto; padding: 18px 20px;
|
|
238
|
-
.content.panel-open { padding-bottom: 300px; }
|
|
175
|
+
.content { flex: 1; overflow-y: auto; padding: 18px 20px; }
|
|
239
176
|
|
|
240
177
|
/* ── Feature cards ───────────────────────────────────────────────────────── */
|
|
241
178
|
.features-grid {
|
|
@@ -482,56 +419,57 @@
|
|
|
482
419
|
|
|
483
420
|
/* ── Test-type cards inside feature detail ───────────────────────────────── */
|
|
484
421
|
.test-type-grid {
|
|
485
|
-
display:
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
align-items: center;
|
|
422
|
+
display: grid;
|
|
423
|
+
grid-template-columns: repeat(4, 1fr);
|
|
424
|
+
gap: 10px;
|
|
425
|
+
margin-bottom: 20px;
|
|
490
426
|
}
|
|
491
427
|
.test-type-card {
|
|
492
|
-
display: flex;
|
|
493
|
-
align-items: center;
|
|
494
|
-
gap: 8px;
|
|
495
428
|
background: var(--bg-card);
|
|
496
429
|
border: 1px solid var(--border);
|
|
497
|
-
border-left: 3px solid transparent;
|
|
498
430
|
border-radius: 8px;
|
|
499
|
-
padding:
|
|
431
|
+
padding: 14px 16px;
|
|
500
432
|
cursor: pointer;
|
|
501
433
|
transition: background 0.15s, border-color 0.15s;
|
|
502
|
-
|
|
434
|
+
position: relative;
|
|
435
|
+
overflow: hidden;
|
|
436
|
+
}
|
|
437
|
+
.test-type-card:hover { background: var(--bg-hover); border-color: var(--dim); }
|
|
438
|
+
.test-type-card .tt-accent {
|
|
439
|
+
position: absolute; top: 0; left: 0; right: 0; height: 3px;
|
|
503
440
|
}
|
|
504
|
-
.test-type-card:hover { background: var(--bg-hover); }
|
|
505
|
-
.test-type-card .tt-accent { display: none; } /* replaced by border-left */
|
|
506
441
|
.test-type-card .tt-label {
|
|
507
442
|
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
508
|
-
color: var(--muted);
|
|
443
|
+
color: var(--muted); margin-bottom: 6px; margin-top: 2px;
|
|
509
444
|
}
|
|
510
445
|
.test-type-card .tt-count {
|
|
511
|
-
font-size:
|
|
446
|
+
font-size: 26px; font-weight: 700; line-height: 1;
|
|
447
|
+
margin-bottom: 4px;
|
|
512
448
|
}
|
|
513
449
|
.test-type-card .tt-sub {
|
|
514
|
-
font-size: 11px; color: var(--dim);
|
|
450
|
+
font-size: 11px; color: var(--dim);
|
|
451
|
+
}
|
|
452
|
+
.test-type-card.tt-empty .tt-count { color: var(--dim); }
|
|
453
|
+
.test-type-card.tt-empty { opacity: 0.7; }
|
|
454
|
+
.test-type-card.tt-active {
|
|
455
|
+
background: var(--bg-hover);
|
|
456
|
+
border-width: 2px;
|
|
515
457
|
}
|
|
516
|
-
.test-type-card.tt-empty { opacity: 0.65; }
|
|
517
|
-
.test-type-card.tt-active { background: var(--bg-hover); }
|
|
518
458
|
.test-type-card.tt-failed {
|
|
519
459
|
border-color: var(--red) !important;
|
|
460
|
+
border-width: 2px;
|
|
520
461
|
background: rgba(248, 81, 73, 0.06);
|
|
521
462
|
}
|
|
522
|
-
.tt-chip-actions { display: flex; gap: 4px; margin-left: 4px; }
|
|
523
463
|
.tt-run-btn {
|
|
524
|
-
display:
|
|
525
|
-
padding: 3px
|
|
464
|
+
display: block; width: 100%; margin-top: 8px;
|
|
465
|
+
padding: 3px 0;
|
|
526
466
|
background: transparent; border: 1px solid var(--border);
|
|
527
467
|
border-radius: 4px; color: var(--muted); font-size: 10px;
|
|
528
|
-
cursor: pointer;
|
|
468
|
+
cursor: pointer; text-align: center;
|
|
529
469
|
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
530
470
|
}
|
|
531
471
|
.tt-run-btn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--dim); }
|
|
532
472
|
.tt-run-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
533
|
-
.tt-fix-btn { border-color: var(--yellow); color: var(--yellow); font-weight: 600; }
|
|
534
|
-
.tt-fix-btn:hover { background: rgba(255,200,0,0.1); color: var(--yellow); border-color: var(--yellow); }
|
|
535
473
|
.file-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
536
474
|
.file-row {
|
|
537
475
|
display: flex;
|
|
@@ -583,10 +521,6 @@
|
|
|
583
521
|
background: rgba(248, 81, 73, 0.07);
|
|
584
522
|
}
|
|
585
523
|
.file-row.has-errors:hover { background: rgba(248, 81, 73, 0.12); }
|
|
586
|
-
.file-agent-spinner { display: inline-block; width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0; }
|
|
587
|
-
.file-agent-spinner.running { border: 2px solid var(--yellow); border-top-color: transparent; animation: spin 0.7s linear infinite; }
|
|
588
|
-
.file-agent-spinner.queued { border: 2px solid var(--dim); border-top-color: transparent; animation: spin 1.5s linear infinite; }
|
|
589
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
590
524
|
.file-row-errors {
|
|
591
525
|
padding: 4px 10px 6px 32px;
|
|
592
526
|
display: flex; flex-direction: column; gap: 3px;
|
|
@@ -668,74 +602,11 @@
|
|
|
668
602
|
font-size: 14px; padding: 3px 6px; border-radius: 4px; line-height: 1;
|
|
669
603
|
}
|
|
670
604
|
.agent-panel-close:hover { background: var(--border); color: var(--text); }
|
|
671
|
-
.agent-panel-copy {
|
|
672
|
-
background: none; border: none; color: var(--dim); cursor: pointer;
|
|
673
|
-
font-size: 13px; padding: 3px 7px; border-radius: 4px; line-height: 1;
|
|
674
|
-
}
|
|
675
|
-
.agent-panel-copy:hover { background: var(--border); color: var(--text); }
|
|
676
|
-
.agent-panel-copy.copied { color: var(--green); }
|
|
677
605
|
.agent-panel-cancel {
|
|
678
606
|
background: none; border: 1px solid var(--yellow); color: var(--yellow);
|
|
679
607
|
cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
680
608
|
}
|
|
681
609
|
.agent-panel-cancel:hover { background: var(--yellow); color: #000; }
|
|
682
|
-
.agent-queue-badge {
|
|
683
|
-
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
684
|
-
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
685
|
-
}
|
|
686
|
-
/* ── Console Tabs ───────────────────────────────────────────────────────── */
|
|
687
|
-
.agent-tabs-bar {
|
|
688
|
-
display: flex; align-items: stretch; overflow-x: auto;
|
|
689
|
-
background: #0a0e15; border-bottom: 1px solid var(--border);
|
|
690
|
-
flex-shrink: 0; min-height: 30px;
|
|
691
|
-
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
692
|
-
}
|
|
693
|
-
.agent-tabs-bar::-webkit-scrollbar { height: 3px; }
|
|
694
|
-
.agent-tabs-bar::-webkit-scrollbar-thumb { background: var(--border); }
|
|
695
|
-
.agent-tab {
|
|
696
|
-
display: flex; align-items: center; gap: 6px;
|
|
697
|
-
padding: 0 8px 0 10px; cursor: pointer;
|
|
698
|
-
white-space: nowrap; border-right: 1px solid var(--border);
|
|
699
|
-
font-size: 11px; color: var(--muted); background: transparent;
|
|
700
|
-
max-width: 200px; flex-shrink: 0; user-select: none;
|
|
701
|
-
transition: background 0.1s, color 0.1s;
|
|
702
|
-
border-bottom: 2px solid transparent;
|
|
703
|
-
}
|
|
704
|
-
.agent-tab:hover { background: var(--bg-hover); color: var(--text); }
|
|
705
|
-
.agent-tab.active { background: var(--bg-card); color: var(--text); border-bottom-color: var(--accent); }
|
|
706
|
-
.agent-tab-title { overflow: hidden; text-overflow: ellipsis; max-width: 140px; }
|
|
707
|
-
.agent-tab-close {
|
|
708
|
-
background: none; border: none; cursor: pointer; color: var(--dim);
|
|
709
|
-
font-size: 13px; padding: 0 2px; line-height: 1; flex-shrink: 0;
|
|
710
|
-
border-radius: 3px; margin-left: 2px; opacity: 0;
|
|
711
|
-
}
|
|
712
|
-
.agent-tab:hover .agent-tab-close { opacity: 1; }
|
|
713
|
-
.agent-tab-close:hover { background: var(--border); color: var(--text); }
|
|
714
|
-
.tab-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
|
715
|
-
.tab-dot-running { background: var(--yellow); animation: tab-pulse 1s ease-in-out infinite; }
|
|
716
|
-
.tab-dot-ok { background: var(--green); }
|
|
717
|
-
.tab-dot-error { background: var(--red); }
|
|
718
|
-
.tab-dot-info { background: var(--muted); }
|
|
719
|
-
@keyframes tab-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
720
|
-
#termBtn.term-error { color: var(--red); border-color: var(--red); }
|
|
721
|
-
.file-row-more-btn {
|
|
722
|
-
background: none; border: none; cursor: pointer; font-size: 14px; color: var(--dim);
|
|
723
|
-
padding: 0 4px; line-height: 1; opacity: 0; transition: opacity 0.1s;
|
|
724
|
-
}
|
|
725
|
-
.file-row:hover .file-row-more-btn { opacity: 1; }
|
|
726
|
-
.file-row-more-wrap { position: relative; display: inline-block; }
|
|
727
|
-
.file-row-more-dropdown {
|
|
728
|
-
display: none; position: absolute; right: 0; top: 100%; z-index: 200;
|
|
729
|
-
background: var(--card); border: 1px solid var(--border); border-radius: 6px;
|
|
730
|
-
min-width: 220px; padding: 4px 0; box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
731
|
-
}
|
|
732
|
-
.file-row-more-dropdown.open { display: block; }
|
|
733
|
-
.file-row-more-item {
|
|
734
|
-
display: block; width: 100%; text-align: left; background: none; border: none;
|
|
735
|
-
cursor: pointer; padding: 7px 14px; font-size: 12px; color: var(--text);
|
|
736
|
-
white-space: nowrap;
|
|
737
|
-
}
|
|
738
|
-
.file-row-more-item:hover { background: var(--border); }
|
|
739
610
|
.agent-terminal {
|
|
740
611
|
flex: 1;
|
|
741
612
|
overflow-y: auto;
|
|
@@ -748,6 +619,93 @@
|
|
|
748
619
|
.agent-line.err { color: var(--red); }
|
|
749
620
|
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
750
621
|
|
|
622
|
+
/* ── E2E Plan ──────────────────────────────────────────────────────────── */
|
|
623
|
+
.e2e-plan-container { display: flex; flex-direction: column; gap: 12px; }
|
|
624
|
+
.e2e-case {
|
|
625
|
+
background: var(--bg-card); border: 1px solid var(--border);
|
|
626
|
+
border-radius: 8px; padding: 14px 16px;
|
|
627
|
+
transition: border-color 0.15s, background 0.15s;
|
|
628
|
+
}
|
|
629
|
+
.e2e-case.approved { border-color: var(--green); }
|
|
630
|
+
.e2e-case.rejected { border-color: var(--red); opacity: 0.5; }
|
|
631
|
+
.e2e-case.written { border-color: var(--blue); }
|
|
632
|
+
.e2e-case.passed { border-color: var(--green); background: rgba(63,185,80,0.06); }
|
|
633
|
+
.e2e-case.failed { border-color: var(--red); background: rgba(248,81,73,0.06); }
|
|
634
|
+
.e2e-case-header {
|
|
635
|
+
display: flex; align-items: center; justify-content: space-between;
|
|
636
|
+
margin-bottom: 6px; gap: 8px;
|
|
637
|
+
}
|
|
638
|
+
.e2e-case-title { font-size: 13px; font-weight: 600; flex: 1; }
|
|
639
|
+
.e2e-case-id { font-size: 11px; color: var(--dim); font-family: monospace; }
|
|
640
|
+
.e2e-case-status {
|
|
641
|
+
font-size: 11px; font-weight: 600; padding: 2px 8px;
|
|
642
|
+
border-radius: 4px; text-transform: uppercase;
|
|
643
|
+
}
|
|
644
|
+
.e2e-case-desc { font-size: 12px; color: var(--muted); margin-bottom: 10px; line-height: 1.4; }
|
|
645
|
+
.e2e-case-steps {
|
|
646
|
+
font-size: 12px; color: var(--text); padding-left: 18px;
|
|
647
|
+
margin-bottom: 6px; line-height: 1.6;
|
|
648
|
+
}
|
|
649
|
+
.e2e-case-steps li { margin-bottom: 2px; }
|
|
650
|
+
.e2e-case-expected {
|
|
651
|
+
font-size: 12px; color: var(--muted); padding-left: 18px;
|
|
652
|
+
margin-bottom: 8px; line-height: 1.6;
|
|
653
|
+
}
|
|
654
|
+
.e2e-case-expected li { margin-bottom: 2px; list-style: disc; }
|
|
655
|
+
.e2e-case-actions { display: flex; gap: 6px; margin-top: 8px; }
|
|
656
|
+
.e2e-btn {
|
|
657
|
+
padding: 4px 12px; border-radius: 4px; font-size: 11px;
|
|
658
|
+
cursor: pointer; font-weight: 600; border: 1px solid; background: transparent;
|
|
659
|
+
transition: background 0.1s, color 0.1s;
|
|
660
|
+
}
|
|
661
|
+
.e2e-btn-approve { border-color: var(--green); color: var(--green); }
|
|
662
|
+
.e2e-btn-approve:hover { background: var(--green); color: #000; }
|
|
663
|
+
.e2e-btn-reject { border-color: var(--red); color: var(--red); }
|
|
664
|
+
.e2e-btn-reject:hover { background: var(--red); color: #fff; }
|
|
665
|
+
.e2e-btn-primary {
|
|
666
|
+
border: none; background: var(--blue); color: #000; font-weight: 700;
|
|
667
|
+
}
|
|
668
|
+
.e2e-btn-primary:hover { filter: brightness(1.1); }
|
|
669
|
+
.e2e-btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
670
|
+
.e2e-btn-secondary {
|
|
671
|
+
border-color: var(--border); color: var(--muted);
|
|
672
|
+
}
|
|
673
|
+
.e2e-btn-secondary:hover { background: var(--bg-hover); color: var(--text); }
|
|
674
|
+
.e2e-error-msg {
|
|
675
|
+
font-size: 11px; color: var(--red); margin-top: 6px;
|
|
676
|
+
padding: 6px 10px; background: rgba(248,81,73,0.08); border-radius: 4px;
|
|
677
|
+
font-family: monospace; white-space: pre-wrap; word-break: break-all;
|
|
678
|
+
}
|
|
679
|
+
.e2e-screenshot-strip {
|
|
680
|
+
display: flex; gap: 8px; overflow-x: auto; padding: 8px 0;
|
|
681
|
+
scrollbar-width: thin;
|
|
682
|
+
}
|
|
683
|
+
.e2e-screenshot {
|
|
684
|
+
width: 180px; height: 120px; object-fit: cover;
|
|
685
|
+
border-radius: 6px; border: 1px solid var(--border);
|
|
686
|
+
cursor: pointer; transition: transform 0.15s, border-color 0.15s;
|
|
687
|
+
flex-shrink: 0;
|
|
688
|
+
}
|
|
689
|
+
.e2e-screenshot:hover { transform: scale(1.05); border-color: var(--blue); }
|
|
690
|
+
.e2e-screenshot-modal {
|
|
691
|
+
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
|
|
692
|
+
background: rgba(0,0,0,0.85); z-index: 300;
|
|
693
|
+
display: flex; align-items: center; justify-content: center;
|
|
694
|
+
cursor: pointer;
|
|
695
|
+
}
|
|
696
|
+
.e2e-screenshot-modal img {
|
|
697
|
+
max-width: 90vw; max-height: 90vh; border-radius: 8px;
|
|
698
|
+
box-shadow: 0 8px 32px rgba(0,0,0,0.5);
|
|
699
|
+
}
|
|
700
|
+
.e2e-batch-bar {
|
|
701
|
+
display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;
|
|
702
|
+
align-items: center;
|
|
703
|
+
}
|
|
704
|
+
.e2e-stats {
|
|
705
|
+
display: flex; gap: 12px; font-size: 12px; color: var(--muted);
|
|
706
|
+
margin-left: auto;
|
|
707
|
+
}
|
|
708
|
+
|
|
751
709
|
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
752
710
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
753
711
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
@@ -762,31 +720,6 @@
|
|
|
762
720
|
<span class="header-time" id="scannedAt"></span>
|
|
763
721
|
<button id="covBtn" onclick="runCoverage()" title="Запустить тесты с coverage">🧪 Coverage</button>
|
|
764
722
|
<button id="termBtn" onclick="toggleAgentPanel()" title="Показать/скрыть терминал агента">📟 Terminal</button>
|
|
765
|
-
<div style="position:relative">
|
|
766
|
-
<button id="agentBtn" onclick="toggleAgentMenu()" title="Настройки агента">🤖 —</button>
|
|
767
|
-
<div id="agentMenu" class="agent-menu">
|
|
768
|
-
<div class="agent-menu-label">AI Агент</div>
|
|
769
|
-
<button class="agent-menu-item" id="amClaude" onclick="setAgent('claude');closeAgentMenu()">
|
|
770
|
-
<span>⚡ Claude Code</span><span class="agent-menu-check" id="amClaudeCheck">✓</span>
|
|
771
|
-
</button>
|
|
772
|
-
<button class="agent-menu-item" id="amCodex" onclick="setAgent('codex');closeAgentMenu()">
|
|
773
|
-
<span>🟢 Codex</span><span class="agent-menu-check" id="amCodexCheck">✓</span>
|
|
774
|
-
</button>
|
|
775
|
-
<div class="agent-menu-divider" id="amModelDivider"></div>
|
|
776
|
-
<div class="agent-menu-label" id="amModelLabel">Модель Claude</div>
|
|
777
|
-
<button class="agent-menu-item" id="amSonnet46" onclick="setModel('claude-sonnet-4-6');closeAgentMenu()">
|
|
778
|
-
<span>sonnet-4-6 <span style="color:var(--dim);font-size:10px">быстрый</span></span><span class="agent-menu-check" id="amSonnet46Check">✓</span>
|
|
779
|
-
</button>
|
|
780
|
-
<button class="agent-menu-item" id="amOpus45" onclick="setModel('claude-opus-4-5');closeAgentMenu()">
|
|
781
|
-
<span>opus-4-5 <span style="color:var(--dim);font-size:10px">умный</span></span><span class="agent-menu-check" id="amOpus45Check">✓</span>
|
|
782
|
-
</button>
|
|
783
|
-
<button class="agent-menu-item" id="amHaiku35" onclick="setModel('claude-haiku-3-5');closeAgentMenu()">
|
|
784
|
-
<span>haiku-3-5 <span style="color:var(--dim);font-size:10px">дешёвый</span></span><span class="agent-menu-check" id="amHaiku35Check">✓</span>
|
|
785
|
-
</button>
|
|
786
|
-
<div class="agent-menu-divider"></div>
|
|
787
|
-
<button class="agent-menu-item" onclick="reauthAgent();closeAgentMenu()">🔑 Перелогиниться</button>
|
|
788
|
-
</div>
|
|
789
|
-
</div>
|
|
790
723
|
<span id="liveDot" title="Connecting…" style="
|
|
791
724
|
width:8px; height:8px; border-radius:50%;
|
|
792
725
|
background:var(--dim); display:inline-block;
|
|
@@ -821,13 +754,9 @@
|
|
|
821
754
|
<div class="agent-panel-header">
|
|
822
755
|
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
823
756
|
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
824
|
-
<span class="agent-queue-badge" id="agentQueueBadge" style="display:none">📋 <span id="agentQueueCount">0</span> в очереди</span>
|
|
825
|
-
<button class="agent-panel-cancel" id="agentQueueClearBtn" onclick="clearAgentQueue()" title="Очистить очередь" style="display:none">🗑 очередь</button>
|
|
826
757
|
<button class="agent-panel-cancel" id="agentCancelBtn" onclick="cancelAgent()" title="Сбросить состояние агента" style="display:none">⏹ сброс</button>
|
|
827
|
-
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
828
758
|
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
829
759
|
</div>
|
|
830
|
-
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
831
760
|
<div class="agent-terminal" id="agentTerminal"></div>
|
|
832
761
|
</div>
|
|
833
762
|
|
|
@@ -842,208 +771,229 @@ let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string
|
|
|
842
771
|
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
843
772
|
let coverageRunning = false;
|
|
844
773
|
let coverageHasError = false;
|
|
774
|
+
let e2ePlan = null;
|
|
775
|
+
let e2ePlanLoading = false;
|
|
845
776
|
|
|
846
|
-
// ───
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
if (
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
} else {
|
|
859
|
-
btn.textContent = '🧪 Coverage';
|
|
860
|
-
}
|
|
777
|
+
// ─── E2E Plan functions ────────────────────────────────────────────────────────
|
|
778
|
+
|
|
779
|
+
async function generateE2ePlan(featureKey) {
|
|
780
|
+
if (agentRunning) { appendTerminalLine('Agent already running', true); return; }
|
|
781
|
+
e2ePlanLoading = true;
|
|
782
|
+
e2ePlan = null;
|
|
783
|
+
renderContent();
|
|
784
|
+
await fetch('/api/e2e/generate-plan', {
|
|
785
|
+
method: 'POST',
|
|
786
|
+
headers: { 'Content-Type': 'application/json' },
|
|
787
|
+
body: JSON.stringify({ featureKey }),
|
|
788
|
+
});
|
|
861
789
|
}
|
|
862
790
|
|
|
863
|
-
async function
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
791
|
+
async function loadE2ePlan(featureKey) {
|
|
792
|
+
try {
|
|
793
|
+
const res = await fetch('/api/e2e/plan/' + encodeURIComponent(featureKey));
|
|
794
|
+
if (res.ok) { e2ePlan = await res.json(); }
|
|
795
|
+
else { e2ePlan = null; }
|
|
796
|
+
} catch { e2ePlan = null; }
|
|
867
797
|
}
|
|
868
798
|
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
let activeSessionId = null; // currently viewed tab
|
|
877
|
-
let runningSessionId = null; // tab that is currently receiving output
|
|
878
|
-
const SESSION_MAX = 25;
|
|
879
|
-
const SESSIONS_KEY = 'viberadar_sessions';
|
|
880
|
-
|
|
881
|
-
function _sessionId() {
|
|
882
|
-
return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
799
|
+
async function reviewE2eCase(featureKey, testCaseId, status) {
|
|
800
|
+
const res = await fetch('/api/e2e/review', {
|
|
801
|
+
method: 'POST',
|
|
802
|
+
headers: { 'Content-Type': 'application/json' },
|
|
803
|
+
body: JSON.stringify({ featureKey, testCaseId, status }),
|
|
804
|
+
});
|
|
805
|
+
if (res.ok) { e2ePlan = (await res.json()).plan; renderContent(); }
|
|
883
806
|
}
|
|
884
807
|
|
|
885
|
-
function
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
})
|
|
890
|
-
|
|
891
|
-
|
|
808
|
+
async function reviewAllE2e(featureKey, status) {
|
|
809
|
+
const res = await fetch('/api/e2e/review-all', {
|
|
810
|
+
method: 'POST',
|
|
811
|
+
headers: { 'Content-Type': 'application/json' },
|
|
812
|
+
body: JSON.stringify({ featureKey, status }),
|
|
813
|
+
});
|
|
814
|
+
if (res.ok) { e2ePlan = (await res.json()).plan; renderContent(); }
|
|
892
815
|
}
|
|
893
816
|
|
|
894
|
-
function
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
if (s.status === 'running') {
|
|
902
|
-
s.status = 'error';
|
|
903
|
-
s.lines.push({ text: '⚡ Прервано (перезагрузка страницы)', isError: true });
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
if (consoleSessions.length > 0) {
|
|
907
|
-
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
908
|
-
renderTabs();
|
|
909
|
-
renderActiveSession();
|
|
910
|
-
}
|
|
911
|
-
} catch {}
|
|
817
|
+
async function writeE2eTests(featureKey) {
|
|
818
|
+
if (agentRunning) { appendTerminalLine('Agent already running', true); return; }
|
|
819
|
+
await fetch('/api/e2e/write-tests', {
|
|
820
|
+
method: 'POST',
|
|
821
|
+
headers: { 'Content-Type': 'application/json' },
|
|
822
|
+
body: JSON.stringify({ featureKey }),
|
|
823
|
+
});
|
|
912
824
|
}
|
|
913
825
|
|
|
914
|
-
function
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
921
|
-
renderTabs();
|
|
922
|
-
renderActiveSession();
|
|
923
|
-
saveSessions();
|
|
924
|
-
return s.id;
|
|
826
|
+
async function runE2eTests(featureKey) {
|
|
827
|
+
await fetch('/api/e2e/run-tests', {
|
|
828
|
+
method: 'POST',
|
|
829
|
+
headers: { 'Content-Type': 'application/json' },
|
|
830
|
+
body: JSON.stringify({ featureKey }),
|
|
831
|
+
});
|
|
925
832
|
}
|
|
926
833
|
|
|
927
|
-
function
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
document.getElementById('agentPanelStatus').textContent = statusText;
|
|
937
|
-
}
|
|
938
|
-
renderTabs();
|
|
939
|
-
renderActiveSession();
|
|
834
|
+
function openScreenshot(src) {
|
|
835
|
+
const modal = document.createElement('div');
|
|
836
|
+
modal.className = 'e2e-screenshot-modal';
|
|
837
|
+
modal.innerHTML = '<img src="' + src + '" />';
|
|
838
|
+
modal.onclick = () => modal.remove();
|
|
839
|
+
document.addEventListener('keydown', function handler(e) {
|
|
840
|
+
if (e.key === 'Escape') { modal.remove(); document.removeEventListener('keydown', handler); }
|
|
841
|
+
});
|
|
842
|
+
document.body.appendChild(modal);
|
|
940
843
|
}
|
|
941
844
|
|
|
942
|
-
function
|
|
943
|
-
const
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
saveSessions();
|
|
845
|
+
function statusBadge(status) {
|
|
846
|
+
const colors = {
|
|
847
|
+
pending: 'var(--muted)', approved: 'var(--green)', rejected: 'var(--red)',
|
|
848
|
+
written: 'var(--blue)', passed: 'var(--green)', failed: 'var(--red)',
|
|
849
|
+
};
|
|
850
|
+
const icons = {
|
|
851
|
+
pending: '⏳', approved: '✅', rejected: '❌',
|
|
852
|
+
written: '✍', passed: '✅', failed: '❌',
|
|
853
|
+
};
|
|
854
|
+
return '<span class="e2e-case-status" style="color:' + (colors[status] || 'var(--muted)') + '">'
|
|
855
|
+
+ (icons[status] || '') + ' ' + status + '</span>';
|
|
954
856
|
}
|
|
955
857
|
|
|
956
|
-
function
|
|
957
|
-
const
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
858
|
+
function renderE2eTestCase(tc, featureKey) {
|
|
859
|
+
const screenshotHtml = (tc.screenshotPaths && tc.screenshotPaths.length > 0)
|
|
860
|
+
? '<div class="e2e-screenshot-strip">' +
|
|
861
|
+
tc.screenshotPaths.map(p =>
|
|
862
|
+
'<img class="e2e-screenshot" src="/api/e2e/screenshot/' + encodeURI(p) + '" onclick="event.stopPropagation();openScreenshot(this.src)" />'
|
|
863
|
+
).join('') + '</div>'
|
|
864
|
+
: '';
|
|
865
|
+
|
|
866
|
+
const errorHtml = tc.lastError
|
|
867
|
+
? '<div class="e2e-error-msg">' + tc.lastError.replace(/</g, '<').slice(0, 500) + '</div>'
|
|
868
|
+
: '';
|
|
869
|
+
|
|
870
|
+
const canReview = tc.status === 'pending' || tc.status === 'approved' || tc.status === 'rejected';
|
|
871
|
+
const actionsHtml = canReview
|
|
872
|
+
? '<div class="e2e-case-actions">' +
|
|
873
|
+
(tc.status !== 'approved'
|
|
874
|
+
? '<button class="e2e-btn e2e-btn-approve" onclick="event.stopPropagation();reviewE2eCase(\'' + featureKey + '\',\'' + tc.id + '\',\'approved\')">✅ Approve</button>'
|
|
875
|
+
: '') +
|
|
876
|
+
(tc.status !== 'rejected'
|
|
877
|
+
? '<button class="e2e-btn e2e-btn-reject" onclick="event.stopPropagation();reviewE2eCase(\'' + featureKey + '\',\'' + tc.id + '\',\'rejected\')">❌ Reject</button>'
|
|
878
|
+
: '') +
|
|
879
|
+
'</div>'
|
|
880
|
+
: '';
|
|
881
|
+
|
|
882
|
+
return '<div class="e2e-case ' + tc.status + '">' +
|
|
883
|
+
'<div class="e2e-case-header">' +
|
|
884
|
+
'<span class="e2e-case-id">' + tc.id + '</span>' +
|
|
885
|
+
'<span class="e2e-case-title">' + tc.name + '</span>' +
|
|
886
|
+
statusBadge(tc.status) +
|
|
887
|
+
'</div>' +
|
|
888
|
+
'<div class="e2e-case-desc">' + tc.description + '</div>' +
|
|
889
|
+
'<div style="font-size:11px;color:var(--muted);margin-bottom:4px;font-weight:600">Steps:</div>' +
|
|
890
|
+
'<ol class="e2e-case-steps">' + tc.steps.map(s => '<li>' + s + '</li>').join('') + '</ol>' +
|
|
891
|
+
'<div style="font-size:11px;color:var(--muted);margin-bottom:4px;font-weight:600">Expected:</div>' +
|
|
892
|
+
'<ul class="e2e-case-expected">' + tc.expectedResults.map(r => '<li>' + r + '</li>').join('') + '</ul>' +
|
|
893
|
+
screenshotHtml +
|
|
894
|
+
errorHtml +
|
|
895
|
+
actionsHtml +
|
|
896
|
+
'</div>';
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
function renderE2ePlanView(c, featureKey) {
|
|
900
|
+
const feat = (D.features || []).find(f => f.key === featureKey);
|
|
901
|
+
if (!feat) return;
|
|
902
|
+
|
|
903
|
+
if (e2ePlanLoading) {
|
|
904
|
+
c.innerHTML =
|
|
905
|
+
'<div class="drill-header">' +
|
|
906
|
+
'<button class="back-btn" onclick="drillTestType=null;renderContent()">← ' + feat.label + '</button>' +
|
|
907
|
+
'<div class="drill-title"><span>🎭</span><span>E2E План</span></div>' +
|
|
908
|
+
'</div>' +
|
|
909
|
+
'<div style="padding:40px;text-align:center;color:var(--muted)">' +
|
|
910
|
+
'<div style="font-size:28px;margin-bottom:12px">⏳</div>' +
|
|
911
|
+
'AI-агент генерирует план E2E тестов…' +
|
|
912
|
+
'</div>';
|
|
913
|
+
return;
|
|
964
914
|
}
|
|
965
|
-
|
|
966
|
-
if (
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
915
|
+
|
|
916
|
+
if (!e2ePlan) {
|
|
917
|
+
c.innerHTML =
|
|
918
|
+
'<div class="drill-header">' +
|
|
919
|
+
'<button class="back-btn" onclick="drillTestType=null;renderContent()">← ' + feat.label + '</button>' +
|
|
920
|
+
'<div class="drill-title"><span>🎭</span><span>E2E План</span></div>' +
|
|
921
|
+
'</div>' +
|
|
922
|
+
'<div style="padding:40px;text-align:center;border:1px dashed var(--border);border-radius:8px">' +
|
|
923
|
+
'<div style="font-size:32px;margin-bottom:12px">🎭</div>' +
|
|
924
|
+
'<div style="font-size:15px;color:var(--text);margin-bottom:6px">E2E план ещё не создан</div>' +
|
|
925
|
+
'<div style="font-size:12px;color:var(--dim);margin-bottom:16px">' +
|
|
926
|
+
'AI-агент проанализирует фичу и создаст план тестирования' +
|
|
927
|
+
'</div>' +
|
|
928
|
+
(!D.hasPlaywright ? '<div style="font-size:12px;color:var(--yellow);margin-bottom:16px">⚠️ @playwright/test не найден в package.json — установи перед запуском тестов</div>' : '') +
|
|
929
|
+
'<button class="e2e-btn e2e-btn-primary" style="padding:10px 24px;font-size:13px;border-radius:8px" ' +
|
|
930
|
+
'onclick="generateE2ePlan(\'' + featureKey + '\')"' +
|
|
931
|
+
(!D.agent ? ' disabled title="Сначала выбери AI агента"' : '') + '>' +
|
|
932
|
+
'▶ Сгенерировать план' +
|
|
933
|
+
'</button>' +
|
|
934
|
+
'</div>';
|
|
935
|
+
return;
|
|
977
936
|
}
|
|
978
|
-
}
|
|
979
937
|
|
|
980
|
-
|
|
981
|
-
const
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
938
|
+
const cases = e2ePlan.testCases || [];
|
|
939
|
+
const counts = { pending: 0, approved: 0, rejected: 0, written: 0, passed: 0, failed: 0 };
|
|
940
|
+
cases.forEach(tc => { counts[tc.status] = (counts[tc.status] || 0) + 1; });
|
|
941
|
+
const hasApproved = counts.approved > 0;
|
|
942
|
+
const hasWritten = counts.written + counts.passed + counts.failed > 0;
|
|
943
|
+
|
|
944
|
+
c.innerHTML =
|
|
945
|
+
'<div class="drill-header">' +
|
|
946
|
+
'<button class="back-btn" onclick="drillTestType=null;renderContent()">← ' + feat.label + '</button>' +
|
|
947
|
+
'<div class="drill-title"><span>🎭</span><span>E2E План</span></div>' +
|
|
948
|
+
'<div class="e2e-stats">' +
|
|
949
|
+
'<span>' + cases.length + ' кейсов</span>' +
|
|
950
|
+
(counts.approved > 0 ? '<span style="color:var(--green)">' + counts.approved + ' одобрено</span>' : '') +
|
|
951
|
+
(counts.written > 0 ? '<span style="color:var(--blue)">' + counts.written + ' написано</span>' : '') +
|
|
952
|
+
(counts.passed > 0 ? '<span style="color:var(--green)">' + counts.passed + ' passed</span>' : '') +
|
|
953
|
+
(counts.failed > 0 ? '<span style="color:var(--red)">' + counts.failed + ' failed</span>' : '') +
|
|
954
|
+
'</div>' +
|
|
955
|
+
'</div>' +
|
|
956
|
+
'<div class="e2e-batch-bar">' +
|
|
957
|
+
'<button class="e2e-btn e2e-btn-approve" onclick="reviewAllE2e(\'' + featureKey + '\',\'approved\')">✅ Approve All</button>' +
|
|
958
|
+
'<button class="e2e-btn e2e-btn-reject" onclick="reviewAllE2e(\'' + featureKey + '\',\'rejected\')">❌ Reject All</button>' +
|
|
959
|
+
(hasApproved && D.agent
|
|
960
|
+
? '<button class="e2e-btn e2e-btn-primary" onclick="writeE2eTests(\'' + featureKey + '\')">✍ Написать тесты (' + counts.approved + ')</button>'
|
|
961
|
+
: '') +
|
|
962
|
+
(hasWritten
|
|
963
|
+
? '<button class="e2e-btn e2e-btn-primary" style="background:var(--green)" onclick="runE2eTests(\'' + featureKey + '\')">▶ Запустить E2E</button>'
|
|
964
|
+
: '') +
|
|
965
|
+
'<button class="e2e-btn e2e-btn-secondary" onclick="generateE2ePlan(\'' + featureKey + '\')">🔄 Перегенерировать</button>' +
|
|
966
|
+
'</div>' +
|
|
967
|
+
'<div class="e2e-plan-container">' +
|
|
968
|
+
cases.map(tc => renderE2eTestCase(tc, featureKey)).join('') +
|
|
969
|
+
'</div>';
|
|
986
970
|
}
|
|
987
971
|
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
972
|
+
// ─── Coverage button ───────────────────────────────────────────────────────────
|
|
973
|
+
function updateCovBtn() {
|
|
974
|
+
const btn = document.getElementById('covBtn');
|
|
975
|
+
if (!btn) return;
|
|
976
|
+
btn.className = '';
|
|
977
|
+
btn.disabled = coverageRunning;
|
|
978
|
+
if (coverageRunning) {
|
|
979
|
+
btn.className = 'cov-running';
|
|
980
|
+
btn.textContent = '⏳ Running...';
|
|
981
|
+
} else if (coverageHasError) {
|
|
982
|
+
btn.className = 'cov-error';
|
|
983
|
+
btn.textContent = '❌ Coverage failed';
|
|
984
|
+
} else {
|
|
985
|
+
btn.textContent = '🧪 Coverage';
|
|
986
|
+
}
|
|
1004
987
|
}
|
|
1005
988
|
|
|
1006
|
-
function
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
//
|
|
1010
|
-
const text = s.lines.map(l => {
|
|
1011
|
-
if (l.text !== undefined) return l.text;
|
|
1012
|
-
if (l.html) {
|
|
1013
|
-
const tmp = document.createElement('div');
|
|
1014
|
-
tmp.innerHTML = l.html;
|
|
1015
|
-
return tmp.innerText;
|
|
1016
|
-
}
|
|
1017
|
-
return '';
|
|
1018
|
-
}).filter(Boolean).join('\n');
|
|
1019
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
1020
|
-
const btn = document.getElementById('agentCopyBtn');
|
|
1021
|
-
if (!btn) return;
|
|
1022
|
-
const prev = btn.textContent;
|
|
1023
|
-
btn.textContent = '✓';
|
|
1024
|
-
btn.classList.add('copied');
|
|
1025
|
-
setTimeout(() => { btn.textContent = prev; btn.classList.remove('copied'); }, 1500);
|
|
1026
|
-
});
|
|
989
|
+
async function runCoverage() {
|
|
990
|
+
if (coverageRunning) return;
|
|
991
|
+
await fetch('/api/run-coverage', { method: 'POST' });
|
|
992
|
+
// SSE events will update state
|
|
1027
993
|
}
|
|
1028
994
|
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
if (!term) return;
|
|
1032
|
-
term.innerHTML = '';
|
|
1033
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1034
|
-
if (!s) return;
|
|
1035
|
-
for (const ln of s.lines) {
|
|
1036
|
-
const el = document.createElement('div');
|
|
1037
|
-
if (ln.html) {
|
|
1038
|
-
el.innerHTML = ln.html;
|
|
1039
|
-
} else {
|
|
1040
|
-
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1041
|
-
el.textContent = ln.text;
|
|
1042
|
-
}
|
|
1043
|
-
term.appendChild(el);
|
|
1044
|
-
}
|
|
1045
|
-
term.scrollTop = term.scrollHeight;
|
|
1046
|
-
}
|
|
995
|
+
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
996
|
+
let agentRunning = false;
|
|
1047
997
|
|
|
1048
998
|
async function setAgent(agent) {
|
|
1049
999
|
await fetch('/api/set-agent', {
|
|
@@ -1054,182 +1004,30 @@ async function setAgent(agent) {
|
|
|
1054
1004
|
// scheduleRescan will fire → data-updated → D.agent updates
|
|
1055
1005
|
}
|
|
1056
1006
|
|
|
1057
|
-
async function setModel(model) {
|
|
1058
|
-
await fetch('/api/set-model', {
|
|
1059
|
-
method: 'POST',
|
|
1060
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1061
|
-
body: JSON.stringify({ model }),
|
|
1062
|
-
});
|
|
1063
|
-
// scheduleRescan will fire → data-updated → D.model updates
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
1007
|
function setAgentRunning(val) {
|
|
1067
1008
|
agentRunning = val;
|
|
1068
1009
|
document.getElementById('agentCancelBtn').style.display = val ? 'inline-block' : 'none';
|
|
1069
1010
|
}
|
|
1070
1011
|
|
|
1071
|
-
// Returns array of normalized relative paths affected by a given task
|
|
1072
|
-
function getTaskFilePaths(task, featureKey, filePath) {
|
|
1073
|
-
if ((task === 'write-tests-file' || task === 'fix-tests') && filePath) {
|
|
1074
|
-
return [filePath.replace(/\\/g, '/')];
|
|
1075
|
-
}
|
|
1076
|
-
if ((task === 'write-tests' || task === 'fix-tests-all') && featureKey && D?.modules) {
|
|
1077
|
-
return D.modules
|
|
1078
|
-
.filter(m => m.featureKeys?.includes(featureKey) && m.type !== 'test' && (!m.hasTests || m.testStale))
|
|
1079
|
-
.map(m => m.relativePath.replace(/\\/g, '/'));
|
|
1080
|
-
}
|
|
1081
|
-
return [];
|
|
1082
|
-
}
|
|
1083
|
-
|
|
1084
|
-
// Returns 'running', 'queued', or null for a given relative path
|
|
1085
|
-
function isFileAgentActive(relPath) {
|
|
1086
|
-
const n = relPath.replace(/\\/g, '/');
|
|
1087
|
-
if (agentRunningPaths.has(n)) return 'running';
|
|
1088
|
-
if (agentQueuedPaths.has(n)) return 'queued';
|
|
1089
|
-
return null;
|
|
1090
|
-
}
|
|
1091
|
-
|
|
1092
|
-
function updateQueueBadge(n) {
|
|
1093
|
-
document.getElementById('agentQueueCount').textContent = n;
|
|
1094
|
-
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1095
|
-
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1096
|
-
}
|
|
1097
|
-
|
|
1098
1012
|
async function cancelAgent() {
|
|
1099
1013
|
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1100
1014
|
setAgentRunning(false);
|
|
1101
|
-
updateQueueBadge(0);
|
|
1102
1015
|
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1103
|
-
|
|
1104
|
-
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1105
|
-
updateSessionStatus(runningSessionId, 'error');
|
|
1106
|
-
runningSessionId = null;
|
|
1107
|
-
} else {
|
|
1108
|
-
appendTerminalLine('⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1109
|
-
}
|
|
1110
|
-
}
|
|
1111
|
-
|
|
1112
|
-
async function clearAgentQueue() {
|
|
1113
|
-
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1114
|
-
updateQueueBadge(0);
|
|
1115
|
-
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1116
|
-
}
|
|
1117
|
-
|
|
1118
|
-
// ─── File row more menu ──────────────────────────────────────────────────────
|
|
1119
|
-
let _openFileMenu = null;
|
|
1120
|
-
|
|
1121
|
-
function toggleFileMenu(btn, featureKey, relPath) {
|
|
1122
|
-
const dropdown = btn.nextElementSibling;
|
|
1123
|
-
const isOpen = dropdown.classList.contains('open');
|
|
1124
|
-
// Close any open menu first
|
|
1125
|
-
if (_openFileMenu && _openFileMenu !== dropdown) {
|
|
1126
|
-
_openFileMenu.classList.remove('open');
|
|
1127
|
-
}
|
|
1128
|
-
dropdown.classList.toggle('open', !isOpen);
|
|
1129
|
-
_openFileMenu = isOpen ? null : dropdown;
|
|
1130
|
-
}
|
|
1131
|
-
|
|
1132
|
-
// Close menu on outside click
|
|
1133
|
-
document.addEventListener('click', () => {
|
|
1134
|
-
if (_openFileMenu) { _openFileMenu.classList.remove('open'); _openFileMenu = null; }
|
|
1135
|
-
});
|
|
1136
|
-
|
|
1137
|
-
async function copyPromptForFile(featureKey, relPath, dropdown) {
|
|
1138
|
-
if (dropdown) { dropdown.classList.remove('open'); _openFileMenu = null; }
|
|
1139
|
-
try {
|
|
1140
|
-
const res = await fetch('/api/get-prompt', {
|
|
1141
|
-
method: 'POST',
|
|
1142
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1143
|
-
body: JSON.stringify({ task: 'write-tests-file', featureKey, filePath: relPath }),
|
|
1144
|
-
});
|
|
1145
|
-
const { prompt, error } = await res.json();
|
|
1146
|
-
if (error || !prompt) { alert('Не удалось получить промпт: ' + (error || 'пустой')); return; }
|
|
1147
|
-
await navigator.clipboard.writeText(prompt);
|
|
1148
|
-
// Brief visual feedback in terminal
|
|
1149
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
1150
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
1151
|
-
appendTerminalLine(`📋 Промпт для "${relPath.split('/').pop()}" скопирован в буфер обмена`, false);
|
|
1152
|
-
appendTerminalLine(' Вставь его в другой агент (Claude, Codex, ChatGPT) и запусти вручную.', false);
|
|
1153
|
-
} catch (e) {
|
|
1154
|
-
alert('Ошибка копирования: ' + e.message);
|
|
1155
|
-
}
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
// ─── Agent menu ─────────────────────────────────────────────────────────────
|
|
1159
|
-
const CLAUDE_MODELS = [
|
|
1160
|
-
{ id: 'claude-sonnet-4-6', checkId: 'amSonnet46Check' },
|
|
1161
|
-
{ id: 'claude-opus-4-5', checkId: 'amOpus45Check' },
|
|
1162
|
-
{ id: 'claude-haiku-3-5', checkId: 'amHaiku35Check' },
|
|
1163
|
-
];
|
|
1164
|
-
|
|
1165
|
-
function updateAgentBtn() {
|
|
1166
|
-
const btn = document.getElementById('agentBtn');
|
|
1167
|
-
if (!D) return;
|
|
1168
|
-
const a = D.agent;
|
|
1169
|
-
const m = D.model;
|
|
1170
|
-
// Button label: agent + model shortname
|
|
1171
|
-
let label = a === 'claude' ? '🤖 Claude' : a === 'codex' ? '🤖 Codex' : '🤖 не выбран';
|
|
1172
|
-
if (a === 'claude' && m) {
|
|
1173
|
-
// Show short model name: "sonnet-4-6", "opus-4-5", "haiku-3-5"
|
|
1174
|
-
const short = m.replace('claude-', '');
|
|
1175
|
-
label += ` · ${short}`;
|
|
1176
|
-
}
|
|
1177
|
-
btn.textContent = label;
|
|
1178
|
-
// Update agent checkmarks
|
|
1179
|
-
document.getElementById('amClaudeCheck').classList.toggle('active', a === 'claude');
|
|
1180
|
-
document.getElementById('amCodexCheck').classList.toggle('active', a === 'codex');
|
|
1181
|
-
// Show/hide model section (only for claude)
|
|
1182
|
-
const isClaude = a === 'claude';
|
|
1183
|
-
document.getElementById('amModelDivider').style.display = isClaude ? '' : 'none';
|
|
1184
|
-
document.getElementById('amModelLabel').style.display = isClaude ? '' : 'none';
|
|
1185
|
-
CLAUDE_MODELS.forEach(({ id, checkId }) => {
|
|
1186
|
-
const el = document.getElementById(id);
|
|
1187
|
-
if (el) el.style.display = isClaude ? '' : 'none';
|
|
1188
|
-
document.getElementById(checkId).classList.toggle('active', m === id);
|
|
1189
|
-
});
|
|
1190
|
-
}
|
|
1191
|
-
|
|
1192
|
-
function toggleAgentMenu() {
|
|
1193
|
-
const menu = document.getElementById('agentMenu');
|
|
1194
|
-
const isOpen = menu.classList.contains('open');
|
|
1195
|
-
menu.classList.toggle('open');
|
|
1196
|
-
if (!isOpen) {
|
|
1197
|
-
// Close on click outside
|
|
1198
|
-
setTimeout(() => {
|
|
1199
|
-
document.addEventListener('click', closeAgentMenuOnOutside, { once: true });
|
|
1200
|
-
}, 0);
|
|
1201
|
-
}
|
|
1202
|
-
}
|
|
1203
|
-
function closeAgentMenu() {
|
|
1204
|
-
document.getElementById('agentMenu').classList.remove('open');
|
|
1205
|
-
}
|
|
1206
|
-
function closeAgentMenuOnOutside(e) {
|
|
1207
|
-
if (!e.target.closest('#agentMenu') && !e.target.closest('#agentBtn')) {
|
|
1208
|
-
closeAgentMenu();
|
|
1209
|
-
}
|
|
1016
|
+
appendTerminalLine('⏹ Состояние агента сброшено', false);
|
|
1210
1017
|
}
|
|
1211
1018
|
|
|
1212
|
-
async function
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
appendTerminalLine('⚠ Сначала выбери агента (Claude или Codex)', true);
|
|
1019
|
+
async function runAgentTask(task, featureKey, filePath) {
|
|
1020
|
+
if (agentRunning) {
|
|
1021
|
+
// Show panel so user sees cancel button
|
|
1216
1022
|
document.getElementById('agentPanel').classList.add('open');
|
|
1217
1023
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1024
|
+
appendTerminalLine('⚠️ Агент уже запущен. Нажми ⏹ сброс чтобы отменить.', true);
|
|
1218
1025
|
return;
|
|
1219
1026
|
}
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
document.getElementById('agentPanelTitle').textContent = '🔑 Перелогинивание';
|
|
1223
|
-
document.getElementById('agentPanelStatus').textContent = 'выполняю…';
|
|
1224
|
-
await fetch('/api/agent-reauth', { method: 'POST' });
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
async function runAgentTask(task, featureKey, filePath) {
|
|
1027
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
1028
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1228
1029
|
document.getElementById('agentPanel').classList.add('open');
|
|
1229
1030
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1230
|
-
if (!agentRunning) {
|
|
1231
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1232
|
-
}
|
|
1233
1031
|
await fetch('/api/run-agent', {
|
|
1234
1032
|
method: 'POST',
|
|
1235
1033
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1238,9 +1036,11 @@ async function runAgentTask(task, featureKey, filePath) {
|
|
|
1238
1036
|
}
|
|
1239
1037
|
|
|
1240
1038
|
async function runTests(featureKey, testType) {
|
|
1039
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
1040
|
+
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
1041
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1241
1042
|
document.getElementById('agentPanel').classList.add('open');
|
|
1242
1043
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1243
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1244
1044
|
await fetch('/api/run-tests', {
|
|
1245
1045
|
method: 'POST',
|
|
1246
1046
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1261,11 +1061,12 @@ function toggleAgentPanel() {
|
|
|
1261
1061
|
}
|
|
1262
1062
|
|
|
1263
1063
|
function appendTerminalLine(line, isError, isDim) {
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1064
|
+
const term = document.getElementById('agentTerminal');
|
|
1065
|
+
const el = document.createElement('div');
|
|
1066
|
+
el.className = 'agent-line' + (isError ? ' err' : isDim ? ' dim' : '');
|
|
1067
|
+
el.textContent = line;
|
|
1068
|
+
term.appendChild(el);
|
|
1069
|
+
term.scrollTop = term.scrollHeight;
|
|
1269
1070
|
}
|
|
1270
1071
|
|
|
1271
1072
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
@@ -1312,7 +1113,6 @@ async function init() {
|
|
|
1312
1113
|
coverageHasError = status.coverageError ?? false;
|
|
1313
1114
|
}
|
|
1314
1115
|
updateCovBtn();
|
|
1315
|
-
updateAgentBtn();
|
|
1316
1116
|
|
|
1317
1117
|
document.getElementById('projectName').textContent = D.projectName;
|
|
1318
1118
|
document.getElementById('scannedAt').textContent =
|
|
@@ -1567,29 +1367,29 @@ function renderFeatureCards(c) {
|
|
|
1567
1367
|
function testTypeCard(type, label, icon, color, count, active, featureKey, failedCount = 0) {
|
|
1568
1368
|
const empty = count === 0 && type !== 'source';
|
|
1569
1369
|
const hasFailed = failedCount > 0 && type !== 'source';
|
|
1570
|
-
const
|
|
1571
|
-
const
|
|
1572
|
-
|
|
1573
|
-
|
|
1370
|
+
const hasE2ePlan = type === 'e2e' && D.e2ePlansExist && D.e2ePlansExist.includes(featureKey);
|
|
1371
|
+
const subLabel = empty
|
|
1372
|
+
? (type === 'e2e' && hasE2ePlan ? '📋 план создан' : 'нет тестов')
|
|
1373
|
+
: (type === 'source' ? 'код приложения' : pluralFiles(count));
|
|
1574
1374
|
const runBtn = !empty && type !== 'source' && featureKey
|
|
1575
|
-
? `<button class="tt-run-btn" onclick="event.stopPropagation()
|
|
1576
|
-
: '';
|
|
1577
|
-
const fixAllBtn = hasFailed && D.agent && featureKey
|
|
1578
|
-
? `<button class="tt-run-btn tt-fix-btn" onclick="event.stopPropagation();runAgentTask('fix-tests-all','${featureKey}','${type}')">🔧 починить</button>`
|
|
1375
|
+
? `<button class="tt-run-btn" onclick="event.stopPropagation();${type === 'e2e' ? "runE2eTests('" + featureKey + "')" : "runTests('" + featureKey + "','" + type + "')"}">▶ запустить</button>`
|
|
1579
1376
|
: '';
|
|
1580
|
-
const
|
|
1581
|
-
|
|
1377
|
+
const accentColor = hasFailed && !active ? 'var(--red)' : color;
|
|
1378
|
+
const countColor = hasFailed ? 'var(--red)' : (active || !empty ? color : 'var(--dim)');
|
|
1379
|
+
const failedNote = hasFailed
|
|
1380
|
+
? `<div style="font-size:11px;color:var(--red);margin-top:2px;font-weight:600">❌ ${failedCount} упало</div>`
|
|
1582
1381
|
: '';
|
|
1583
|
-
|
|
1584
|
-
|
|
1382
|
+
// E2E cards should always be clickable (to see/create plan)
|
|
1383
|
+
const forceClickable = type === 'e2e';
|
|
1585
1384
|
return `
|
|
1586
|
-
<div class="test-type-card${empty ? ' tt-empty' : ''}${active ? ' tt-active' : ''}${hasFailed ? ' tt-failed' : ''}"
|
|
1587
|
-
data-testtype="${type}" style="
|
|
1588
|
-
<
|
|
1589
|
-
<
|
|
1590
|
-
<
|
|
1591
|
-
|
|
1592
|
-
${
|
|
1385
|
+
<div class="test-type-card${empty && !forceClickable ? ' tt-empty' : ''}${active ? ' tt-active' : ''}${hasFailed ? ' tt-failed' : ''}"
|
|
1386
|
+
data-testtype="${type}" style="${active ? 'border-color:' + color : ''}${forceClickable && empty ? ';cursor:pointer;opacity:1' : ''}">
|
|
1387
|
+
<div class="tt-accent" style="background:${accentColor}"></div>
|
|
1388
|
+
<div class="tt-label">${icon} ${label}</div>
|
|
1389
|
+
<div class="tt-count" style="color:${countColor}">${count}</div>
|
|
1390
|
+
<div class="tt-sub">${subLabel}</div>
|
|
1391
|
+
${failedNote}
|
|
1392
|
+
${runBtn}
|
|
1593
1393
|
</div>`;
|
|
1594
1394
|
}
|
|
1595
1395
|
|
|
@@ -1597,6 +1397,12 @@ function renderFeatureDetail(c) {
|
|
|
1597
1397
|
const feat = D.features.find(f => f.key === drillFeatureKey);
|
|
1598
1398
|
if (!feat) { backToFeatures(); return; }
|
|
1599
1399
|
|
|
1400
|
+
// E2E plan view override
|
|
1401
|
+
if (drillTestType === 'e2e') {
|
|
1402
|
+
renderE2ePlanView(c, drillFeatureKey);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1600
1406
|
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
1601
1407
|
const src = mods.filter(m => m.type !== 'test');
|
|
1602
1408
|
const tst = mods.filter(m => m.type === 'test');
|
|
@@ -1664,11 +1470,14 @@ function renderFeatureDetail(c) {
|
|
|
1664
1470
|
</div>`;
|
|
1665
1471
|
|
|
1666
1472
|
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
1667
|
-
card.onclick = () => {
|
|
1473
|
+
card.onclick = async () => {
|
|
1668
1474
|
const type = card.dataset.testtype;
|
|
1669
1475
|
drillTestType = (type === 'source') ? null : type; // 'source' tab = null state
|
|
1670
1476
|
activePanelKey = null;
|
|
1671
1477
|
document.getElementById('panel').classList.remove('open');
|
|
1478
|
+
if (type === 'e2e') {
|
|
1479
|
+
await loadE2ePlan(drillFeatureKey);
|
|
1480
|
+
}
|
|
1672
1481
|
renderContent();
|
|
1673
1482
|
};
|
|
1674
1483
|
});
|
|
@@ -1823,16 +1632,12 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1823
1632
|
const dir = parts.slice(0, -1).join('/');
|
|
1824
1633
|
|
|
1825
1634
|
// Test errors from last run
|
|
1826
|
-
const testErr
|
|
1827
|
-
const
|
|
1828
|
-
const icon = agentState
|
|
1829
|
-
? `<span class="file-agent-spinner ${agentState}" title="${agentState === 'running' ? 'Агент работает…' : 'В очереди…'}"></span>`
|
|
1830
|
-
: (testErr ? '❌' : isTest ? '🧪' : (m.testStale ? '⚠️' : m.hasTests ? '✅' : '⬜'));
|
|
1635
|
+
const testErr = isTest && D.testErrors && D.testErrors[relPath];
|
|
1636
|
+
const icon = testErr ? '❌' : isTest ? '🧪' : (m.testStale ? '⚠️' : m.hasTests ? '✅' : '⬜');
|
|
1831
1637
|
const isActive = activePanelKey === m.id;
|
|
1832
1638
|
|
|
1833
1639
|
// Write-test button for source files
|
|
1834
|
-
|
|
1835
|
-
const showAgentBtn = D.agent && !isTest && featureKey && (!m.hasTests || m.testStale);
|
|
1640
|
+
const showAgentBtn = D.agent && !isTest && !m.isInfra && featureKey && (!m.hasTests || m.testStale);
|
|
1836
1641
|
const agentBtnLabel = m.testStale ? '↻ обновить тест' : '✍ написать тест';
|
|
1837
1642
|
const agentBtnTitle = m.testStale ? 'Файл изменился — обновить тесты' : 'Написать тест для этого файла';
|
|
1838
1643
|
const agentBtn = showAgentBtn
|
|
@@ -1863,20 +1668,6 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1863
1668
|
</div>`
|
|
1864
1669
|
: '';
|
|
1865
1670
|
|
|
1866
|
-
// "⋯" more menu — always show for source files in feature context
|
|
1867
|
-
const moreBtn = !isTest && featureKey
|
|
1868
|
-
? `<div class="file-row-more-wrap">
|
|
1869
|
-
<button class="file-row-more-btn" title="Ещё"
|
|
1870
|
-
onclick="event.stopPropagation();toggleFileMenu(this,'${featureKey}','${relPath}')">⋯</button>
|
|
1871
|
-
<div class="file-row-more-dropdown">
|
|
1872
|
-
<button class="file-row-more-item"
|
|
1873
|
-
onclick="event.stopPropagation();copyPromptForFile('${featureKey}','${relPath}',this.closest('.file-row-more-dropdown'))">
|
|
1874
|
-
📋 Скопировать промпт для агента
|
|
1875
|
-
</button>
|
|
1876
|
-
</div>
|
|
1877
|
-
</div>`
|
|
1878
|
-
: '';
|
|
1879
|
-
|
|
1880
1671
|
return `
|
|
1881
1672
|
<div class="file-row${isActive ? ' active' : ''}${testErr ? ' has-errors' : ''}" data-id="${m.id}">
|
|
1882
1673
|
<span class="file-row-icon">${icon}</span>
|
|
@@ -1884,7 +1675,6 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1884
1675
|
${errBadge}
|
|
1885
1676
|
${agentBtn}
|
|
1886
1677
|
${fixBtn}
|
|
1887
|
-
${moreBtn}
|
|
1888
1678
|
<span class="file-row-dir">${dir}</span>
|
|
1889
1679
|
</div>
|
|
1890
1680
|
${errHtml}`;
|
|
@@ -1980,11 +1770,7 @@ function fileItem(m, isTest = false) {
|
|
|
1980
1770
|
const parts = m.relativePath.replace(/\\/g, '/').split('/');
|
|
1981
1771
|
const name = parts[parts.length - 1];
|
|
1982
1772
|
const dir = parts.slice(0, -1).join('/');
|
|
1983
|
-
const
|
|
1984
|
-
const agentStateItem = !isTest ? isFileAgentActive(relPathNorm) : null;
|
|
1985
|
-
const icon = agentStateItem
|
|
1986
|
-
? `<span class="file-agent-spinner ${agentStateItem}" title="${agentStateItem === 'running' ? 'Агент работает…' : 'В очереди…'}"></span>`
|
|
1987
|
-
: (isTest ? '🧪' : (m.hasTests ? '✅' : '⬜'));
|
|
1773
|
+
const icon = isTest ? '🧪' : (m.hasTests ? '✅' : '⬜');
|
|
1988
1774
|
return `
|
|
1989
1775
|
<div class="file-item">
|
|
1990
1776
|
<span class="file-item-icon">${icon}</span>
|
|
@@ -2182,7 +1968,6 @@ async function refreshData() {
|
|
|
2182
1968
|
renderStats();
|
|
2183
1969
|
renderSidebar();
|
|
2184
1970
|
renderContent();
|
|
2185
|
-
updateAgentBtn();
|
|
2186
1971
|
|
|
2187
1972
|
// Re-render drill-down or re-open panel
|
|
2188
1973
|
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
@@ -2236,69 +2021,31 @@ function connectSSE() {
|
|
|
2236
2021
|
setTimeout(() => { coverageHasError = false; updateCovBtn(); }, 8000);
|
|
2237
2022
|
});
|
|
2238
2023
|
|
|
2239
|
-
es.addEventListener('agent-queued', (e) => {
|
|
2240
|
-
const { queueLength, title, task, featureKey, filePath } = JSON.parse(e.data);
|
|
2241
|
-
updateQueueBadge(queueLength);
|
|
2242
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
2243
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
2244
|
-
// Track queued paths for spinner
|
|
2245
|
-
getTaskFilePaths(task, featureKey, filePath).forEach(p => agentQueuedPaths.add(p));
|
|
2246
|
-
renderContent();
|
|
2247
|
-
// Append queue notification to the currently running session (or active)
|
|
2248
|
-
const targetId = runningSessionId || activeSessionId;
|
|
2249
|
-
if (targetId) {
|
|
2250
|
-
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
2251
|
-
}
|
|
2252
|
-
});
|
|
2253
|
-
|
|
2254
2024
|
es.addEventListener('agent-started', (e) => {
|
|
2255
2025
|
setAgentRunning(true);
|
|
2256
|
-
const { title
|
|
2257
|
-
updateQueueBadge(queueLength);
|
|
2258
|
-
// Move paths from queued → running (current task)
|
|
2259
|
-
const startedPaths = getTaskFilePaths(task, featureKey, filePath);
|
|
2260
|
-
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
2261
|
-
renderContent();
|
|
2262
|
-
// Close previous session (queue case: agent-done not fired between tasks)
|
|
2263
|
-
if (runningSessionId) {
|
|
2264
|
-
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
2265
|
-
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
2266
|
-
}
|
|
2267
|
-
const id = createSession(title);
|
|
2268
|
-
runningSessionId = id;
|
|
2026
|
+
const { title } = JSON.parse(e.data);
|
|
2269
2027
|
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
2270
2028
|
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
2029
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
2030
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
2031
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
2271
2032
|
});
|
|
2272
2033
|
|
|
2273
2034
|
es.addEventListener('agent-output', (e) => {
|
|
2274
2035
|
const { line, isError, isDim } = JSON.parse(e.data);
|
|
2275
|
-
|
|
2276
|
-
|
|
2277
|
-
if (activeSessionId === runningSessionId) {
|
|
2278
|
-
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
2279
|
-
}
|
|
2280
|
-
} else {
|
|
2281
|
-
appendTerminalLine(line, !!isError, !!isDim);
|
|
2282
|
-
}
|
|
2036
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
2037
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
2283
2038
|
});
|
|
2284
2039
|
|
|
2285
2040
|
es.addEventListener('agent-done', () => {
|
|
2286
2041
|
setAgentRunning(false);
|
|
2287
|
-
|
|
2288
|
-
agentRunningPaths.clear();
|
|
2289
|
-
agentQueuedPaths.clear();
|
|
2290
|
-
if (runningSessionId) {
|
|
2291
|
-
updateSessionStatus(runningSessionId, 'ok');
|
|
2292
|
-
if (activeSessionId === runningSessionId) {
|
|
2293
|
-
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
2294
|
-
}
|
|
2295
|
-
runningSessionId = null;
|
|
2296
|
-
}
|
|
2042
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
2297
2043
|
renderContent();
|
|
2298
2044
|
});
|
|
2299
2045
|
|
|
2300
2046
|
es.addEventListener('agent-summary', (e) => {
|
|
2301
2047
|
const { passed, failed, files } = JSON.parse(e.data);
|
|
2048
|
+
const term = document.getElementById('agentTerminal');
|
|
2302
2049
|
const allOk = failed === 0;
|
|
2303
2050
|
const box = document.createElement('div');
|
|
2304
2051
|
box.style.cssText = `
|
|
@@ -2315,82 +2062,66 @@ function connectSSE() {
|
|
|
2315
2062
|
</div>
|
|
2316
2063
|
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
2317
2064
|
`;
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
appendToSession(targetId, box);
|
|
2321
|
-
// Mark session status from test results immediately
|
|
2322
|
-
if (runningSessionId === targetId) {
|
|
2323
|
-
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
2324
|
-
}
|
|
2325
|
-
}
|
|
2065
|
+
term.appendChild(box);
|
|
2066
|
+
term.scrollTop = term.scrollHeight;
|
|
2326
2067
|
});
|
|
2327
2068
|
|
|
2328
2069
|
es.addEventListener('agent-error', (e) => {
|
|
2329
2070
|
setAgentRunning(false);
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
const { message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
2334
|
-
|
|
2335
|
-
// Build error node (plain text or auth/install box)
|
|
2336
|
-
let node = null;
|
|
2337
|
-
if (authRequired || notInstalled) {
|
|
2338
|
-
node = document.createElement('div');
|
|
2339
|
-
node.style.cssText = 'margin:10px 0 4px;padding:10px 14px;border-radius:8px;border:1px solid var(--red);background:#2a0d0d;font-family:inherit;';
|
|
2340
|
-
node.innerHTML = `
|
|
2341
|
-
<div style="font-size:13px;font-weight:700;color:var(--red)">❌ ${message || 'Ошибка агента'}</div>
|
|
2342
|
-
${authRequired ? `<button onclick="reauthAgent()" style="margin-top:8px;padding:4px 12px;font-size:12px;background:none;border:1px solid var(--yellow);color:var(--yellow);border-radius:4px;cursor:pointer;">🔑 Перелогиниться</button>` : ''}
|
|
2343
|
-
${notInstalled ? `<div style="margin-top:6px;font-size:11px;color:var(--dim)">После установки перезапусти viberadar</div>` : ''}
|
|
2344
|
-
`;
|
|
2345
|
-
}
|
|
2346
|
-
|
|
2347
|
-
// If no session exists yet (startup check fires before any run), create one
|
|
2348
|
-
const targetId = runningSessionId || (() => {
|
|
2349
|
-
if (authRequired || notInstalled) {
|
|
2350
|
-
const id = createSession('⚠️ Проверка агента', 'error');
|
|
2351
|
-
return id;
|
|
2352
|
-
}
|
|
2353
|
-
return activeSessionId;
|
|
2354
|
-
})();
|
|
2355
|
-
|
|
2356
|
-
if (targetId) {
|
|
2357
|
-
if (node) {
|
|
2358
|
-
appendToSession(targetId, node);
|
|
2359
|
-
} else {
|
|
2360
|
-
appendToSession(targetId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
2361
|
-
}
|
|
2362
|
-
updateSessionStatus(targetId, 'error');
|
|
2363
|
-
if (activeSessionId === targetId) {
|
|
2364
|
-
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
2365
|
-
}
|
|
2366
|
-
}
|
|
2367
|
-
if (runningSessionId) runningSessionId = null;
|
|
2071
|
+
const { message } = JSON.parse(e.data);
|
|
2072
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
2073
|
+
appendTerminalLine('❌ ' + (message || 'Ошибка агента'), true);
|
|
2368
2074
|
});
|
|
2369
2075
|
|
|
2370
2076
|
es.addEventListener('tests-started', (e) => {
|
|
2371
2077
|
const { testType, count } = JSON.parse(e.data);
|
|
2372
|
-
const id = createSession(`🧪 ${testType}`);
|
|
2373
|
-
runningSessionId = id;
|
|
2374
|
-
appendToSession(id, `Запускаю ${testType} тесты (${count} файлов)…`);
|
|
2375
2078
|
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
2376
2079
|
document.getElementById('agentPanelStatus').textContent = `запускаю ${count} файлов…`;
|
|
2377
2080
|
});
|
|
2378
2081
|
|
|
2379
2082
|
es.addEventListener('tests-done', (e) => {
|
|
2380
2083
|
const { passed, failed, testErrors } = JSON.parse(e.data);
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
if (activeSessionId === runningSessionId) {
|
|
2385
|
-
document.getElementById('agentPanelStatus').textContent =
|
|
2386
|
-
failed === 0 ? `✅ ${passed} passed` : `⚠️ ${passed} passed, ${failed} failed`;
|
|
2387
|
-
}
|
|
2388
|
-
runningSessionId = null;
|
|
2389
|
-
}
|
|
2084
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
2085
|
+
failed === 0 ? `✅ ${passed} passed` : `⚠️ ${passed} passed, ${failed} failed`;
|
|
2086
|
+
// Apply testErrors directly from event — no separate fetch needed
|
|
2390
2087
|
D.testErrors = testErrors || {};
|
|
2391
2088
|
renderContent();
|
|
2392
2089
|
});
|
|
2393
2090
|
|
|
2091
|
+
// ── E2E plan events ──────────────────────────────────────────────────────
|
|
2092
|
+
es.addEventListener('e2e-plan-generating', () => {
|
|
2093
|
+
e2ePlanLoading = true;
|
|
2094
|
+
renderContent();
|
|
2095
|
+
});
|
|
2096
|
+
|
|
2097
|
+
es.addEventListener('e2e-plan-ready', (e) => {
|
|
2098
|
+
const data = JSON.parse(e.data);
|
|
2099
|
+
e2ePlanLoading = false;
|
|
2100
|
+
if (drillFeatureKey === data.featureKey) {
|
|
2101
|
+
e2ePlan = data.plan;
|
|
2102
|
+
renderContent();
|
|
2103
|
+
}
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
es.addEventListener('e2e-plan-error', (e) => {
|
|
2107
|
+
e2ePlanLoading = false;
|
|
2108
|
+
const data = JSON.parse(e.data);
|
|
2109
|
+
if (drillFeatureKey === data.featureKey) {
|
|
2110
|
+
e2ePlan = null;
|
|
2111
|
+
renderContent();
|
|
2112
|
+
}
|
|
2113
|
+
});
|
|
2114
|
+
|
|
2115
|
+
es.addEventListener('e2e-tests-done', (e) => {
|
|
2116
|
+
const data = JSON.parse(e.data);
|
|
2117
|
+
if (drillFeatureKey === data.featureKey) {
|
|
2118
|
+
e2ePlan = data.plan;
|
|
2119
|
+
renderContent();
|
|
2120
|
+
}
|
|
2121
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
2122
|
+
data.failed === 0 ? `✅ E2E: ${data.passed} passed` : `⚠️ E2E: ${data.passed}/${data.failed}`;
|
|
2123
|
+
});
|
|
2124
|
+
|
|
2394
2125
|
es.onerror = () => {
|
|
2395
2126
|
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
2396
2127
|
es.close();
|
|
@@ -2398,15 +2129,7 @@ function connectSSE() {
|
|
|
2398
2129
|
};
|
|
2399
2130
|
}
|
|
2400
2131
|
|
|
2401
|
-
init().then(() =>
|
|
2402
|
-
restoreSessions();
|
|
2403
|
-
connectSSE();
|
|
2404
|
-
// Sync content padding with terminal panel open state automatically
|
|
2405
|
-
new MutationObserver(() => {
|
|
2406
|
-
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
2407
|
-
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
2408
|
-
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
2409
|
-
});
|
|
2132
|
+
init().then(() => connectSSE());
|
|
2410
2133
|
</script>
|
|
2411
2134
|
</body>
|
|
2412
2135
|
</html>
|