viberadar 0.3.45 → 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 +418 -696
- 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 {
|
|
@@ -483,48 +420,56 @@
|
|
|
483
420
|
/* ── Test-type cards inside feature detail ───────────────────────────────── */
|
|
484
421
|
.test-type-grid {
|
|
485
422
|
display: grid;
|
|
486
|
-
grid-template-columns: repeat(
|
|
423
|
+
grid-template-columns: repeat(4, 1fr);
|
|
487
424
|
gap: 10px;
|
|
488
|
-
margin-bottom:
|
|
425
|
+
margin-bottom: 20px;
|
|
489
426
|
}
|
|
490
427
|
.test-type-card {
|
|
491
|
-
display: flex;
|
|
492
|
-
flex-direction: column;
|
|
493
428
|
background: var(--bg-card);
|
|
494
429
|
border: 1px solid var(--border);
|
|
495
|
-
border-top: 3px solid transparent;
|
|
496
430
|
border-radius: 8px;
|
|
497
|
-
padding:
|
|
431
|
+
padding: 14px 16px;
|
|
498
432
|
cursor: pointer;
|
|
499
433
|
transition: background 0.15s, border-color 0.15s;
|
|
500
|
-
|
|
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;
|
|
440
|
+
}
|
|
441
|
+
.test-type-card .tt-label {
|
|
442
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
443
|
+
color: var(--muted); margin-bottom: 6px; margin-top: 2px;
|
|
444
|
+
}
|
|
445
|
+
.test-type-card .tt-count {
|
|
446
|
+
font-size: 26px; font-weight: 700; line-height: 1;
|
|
447
|
+
margin-bottom: 4px;
|
|
448
|
+
}
|
|
449
|
+
.test-type-card .tt-sub {
|
|
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;
|
|
501
457
|
}
|
|
502
|
-
.test-type-card:hover { background: var(--bg-hover); }
|
|
503
|
-
.test-type-card.tt-active { background: var(--bg-hover); border-color: var(--border); }
|
|
504
|
-
.test-type-card.tt-empty { opacity: 0.6; }
|
|
505
458
|
.test-type-card.tt-failed {
|
|
506
459
|
border-color: var(--red) !important;
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
.tt-label { font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--muted); }
|
|
511
|
-
.tt-failed-badge { font-size: 10px; color: var(--red); font-weight: 700; }
|
|
512
|
-
.tt-count { font-size: 28px; font-weight: 700; line-height: 1; margin-bottom: 2px; }
|
|
513
|
-
.tt-sub { font-size: 11px; color: var(--dim); }
|
|
514
|
-
.tt-card-actions { display: flex; flex-direction: column; gap: 5px; margin-top: 10px; }
|
|
460
|
+
border-width: 2px;
|
|
461
|
+
background: rgba(248, 81, 73, 0.06);
|
|
462
|
+
}
|
|
515
463
|
.tt-run-btn {
|
|
516
|
-
width: 100%;
|
|
464
|
+
display: block; width: 100%; margin-top: 8px;
|
|
465
|
+
padding: 3px 0;
|
|
517
466
|
background: transparent; border: 1px solid var(--border);
|
|
518
|
-
border-radius:
|
|
467
|
+
border-radius: 4px; color: var(--muted); font-size: 10px;
|
|
519
468
|
cursor: pointer; text-align: center;
|
|
520
469
|
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
521
470
|
}
|
|
522
471
|
.tt-run-btn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--dim); }
|
|
523
472
|
.tt-run-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
524
|
-
.tt-fix-btn { border-color: var(--yellow); color: var(--yellow); font-weight: 600; }
|
|
525
|
-
.tt-fix-btn:hover { background: rgba(255,200,0,0.1); color: var(--yellow); border-color: var(--yellow); }
|
|
526
|
-
.tt-write-btn { border-color: var(--accent); color: var(--accent); }
|
|
527
|
-
.tt-write-btn:hover { background: rgba(88,166,255,0.1); color: var(--accent); border-color: var(--accent); }
|
|
528
473
|
.file-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
529
474
|
.file-row {
|
|
530
475
|
display: flex;
|
|
@@ -576,10 +521,6 @@
|
|
|
576
521
|
background: rgba(248, 81, 73, 0.07);
|
|
577
522
|
}
|
|
578
523
|
.file-row.has-errors:hover { background: rgba(248, 81, 73, 0.12); }
|
|
579
|
-
.file-agent-spinner { display: inline-block; width: 14px; height: 14px; border-radius: 50%; flex-shrink: 0; }
|
|
580
|
-
.file-agent-spinner.running { border: 2px solid var(--yellow); border-top-color: transparent; animation: spin 0.7s linear infinite; }
|
|
581
|
-
.file-agent-spinner.queued { border: 2px solid var(--dim); border-top-color: transparent; animation: spin 1.5s linear infinite; }
|
|
582
|
-
@keyframes spin { to { transform: rotate(360deg); } }
|
|
583
524
|
.file-row-errors {
|
|
584
525
|
padding: 4px 10px 6px 32px;
|
|
585
526
|
display: flex; flex-direction: column; gap: 3px;
|
|
@@ -661,74 +602,11 @@
|
|
|
661
602
|
font-size: 14px; padding: 3px 6px; border-radius: 4px; line-height: 1;
|
|
662
603
|
}
|
|
663
604
|
.agent-panel-close:hover { background: var(--border); color: var(--text); }
|
|
664
|
-
.agent-panel-copy {
|
|
665
|
-
background: none; border: none; color: var(--dim); cursor: pointer;
|
|
666
|
-
font-size: 13px; padding: 3px 7px; border-radius: 4px; line-height: 1;
|
|
667
|
-
}
|
|
668
|
-
.agent-panel-copy:hover { background: var(--border); color: var(--text); }
|
|
669
|
-
.agent-panel-copy.copied { color: var(--green); }
|
|
670
605
|
.agent-panel-cancel {
|
|
671
606
|
background: none; border: 1px solid var(--yellow); color: var(--yellow);
|
|
672
607
|
cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
673
608
|
}
|
|
674
609
|
.agent-panel-cancel:hover { background: var(--yellow); color: #000; }
|
|
675
|
-
.agent-queue-badge {
|
|
676
|
-
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
677
|
-
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
678
|
-
}
|
|
679
|
-
/* ── Console Tabs ───────────────────────────────────────────────────────── */
|
|
680
|
-
.agent-tabs-bar {
|
|
681
|
-
display: flex; align-items: stretch; overflow-x: auto;
|
|
682
|
-
background: #0a0e15; border-bottom: 1px solid var(--border);
|
|
683
|
-
flex-shrink: 0; min-height: 30px;
|
|
684
|
-
scrollbar-width: thin; scrollbar-color: var(--border) transparent;
|
|
685
|
-
}
|
|
686
|
-
.agent-tabs-bar::-webkit-scrollbar { height: 3px; }
|
|
687
|
-
.agent-tabs-bar::-webkit-scrollbar-thumb { background: var(--border); }
|
|
688
|
-
.agent-tab {
|
|
689
|
-
display: flex; align-items: center; gap: 6px;
|
|
690
|
-
padding: 0 8px 0 10px; cursor: pointer;
|
|
691
|
-
white-space: nowrap; border-right: 1px solid var(--border);
|
|
692
|
-
font-size: 11px; color: var(--muted); background: transparent;
|
|
693
|
-
max-width: 200px; flex-shrink: 0; user-select: none;
|
|
694
|
-
transition: background 0.1s, color 0.1s;
|
|
695
|
-
border-bottom: 2px solid transparent;
|
|
696
|
-
}
|
|
697
|
-
.agent-tab:hover { background: var(--bg-hover); color: var(--text); }
|
|
698
|
-
.agent-tab.active { background: var(--bg-card); color: var(--text); border-bottom-color: var(--accent); }
|
|
699
|
-
.agent-tab-title { overflow: hidden; text-overflow: ellipsis; max-width: 140px; }
|
|
700
|
-
.agent-tab-close {
|
|
701
|
-
background: none; border: none; cursor: pointer; color: var(--dim);
|
|
702
|
-
font-size: 13px; padding: 0 2px; line-height: 1; flex-shrink: 0;
|
|
703
|
-
border-radius: 3px; margin-left: 2px; opacity: 0;
|
|
704
|
-
}
|
|
705
|
-
.agent-tab:hover .agent-tab-close { opacity: 1; }
|
|
706
|
-
.agent-tab-close:hover { background: var(--border); color: var(--text); }
|
|
707
|
-
.tab-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
|
|
708
|
-
.tab-dot-running { background: var(--yellow); animation: tab-pulse 1s ease-in-out infinite; }
|
|
709
|
-
.tab-dot-ok { background: var(--green); }
|
|
710
|
-
.tab-dot-error { background: var(--red); }
|
|
711
|
-
.tab-dot-info { background: var(--muted); }
|
|
712
|
-
@keyframes tab-pulse { 0%,100% { opacity: 1; } 50% { opacity: 0.35; } }
|
|
713
|
-
#termBtn.term-error { color: var(--red); border-color: var(--red); }
|
|
714
|
-
.file-row-more-btn {
|
|
715
|
-
background: none; border: none; cursor: pointer; font-size: 14px; color: var(--dim);
|
|
716
|
-
padding: 0 4px; line-height: 1; opacity: 0; transition: opacity 0.1s;
|
|
717
|
-
}
|
|
718
|
-
.file-row:hover .file-row-more-btn { opacity: 1; }
|
|
719
|
-
.file-row-more-wrap { position: relative; display: inline-block; }
|
|
720
|
-
.file-row-more-dropdown {
|
|
721
|
-
display: none; position: absolute; right: 0; top: 100%; z-index: 200;
|
|
722
|
-
background: var(--card); border: 1px solid var(--border); border-radius: 6px;
|
|
723
|
-
min-width: 220px; padding: 4px 0; box-shadow: 0 4px 12px rgba(0,0,0,0.4);
|
|
724
|
-
}
|
|
725
|
-
.file-row-more-dropdown.open { display: block; }
|
|
726
|
-
.file-row-more-item {
|
|
727
|
-
display: block; width: 100%; text-align: left; background: none; border: none;
|
|
728
|
-
cursor: pointer; padding: 7px 14px; font-size: 12px; color: var(--text);
|
|
729
|
-
white-space: nowrap;
|
|
730
|
-
}
|
|
731
|
-
.file-row-more-item:hover { background: var(--border); }
|
|
732
610
|
.agent-terminal {
|
|
733
611
|
flex: 1;
|
|
734
612
|
overflow-y: auto;
|
|
@@ -741,6 +619,93 @@
|
|
|
741
619
|
.agent-line.err { color: var(--red); }
|
|
742
620
|
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
743
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
|
+
|
|
744
709
|
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
745
710
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
746
711
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
@@ -755,31 +720,6 @@
|
|
|
755
720
|
<span class="header-time" id="scannedAt"></span>
|
|
756
721
|
<button id="covBtn" onclick="runCoverage()" title="Запустить тесты с coverage">🧪 Coverage</button>
|
|
757
722
|
<button id="termBtn" onclick="toggleAgentPanel()" title="Показать/скрыть терминал агента">📟 Terminal</button>
|
|
758
|
-
<div style="position:relative">
|
|
759
|
-
<button id="agentBtn" onclick="toggleAgentMenu()" title="Настройки агента">🤖 —</button>
|
|
760
|
-
<div id="agentMenu" class="agent-menu">
|
|
761
|
-
<div class="agent-menu-label">AI Агент</div>
|
|
762
|
-
<button class="agent-menu-item" id="amClaude" onclick="setAgent('claude');closeAgentMenu()">
|
|
763
|
-
<span>⚡ Claude Code</span><span class="agent-menu-check" id="amClaudeCheck">✓</span>
|
|
764
|
-
</button>
|
|
765
|
-
<button class="agent-menu-item" id="amCodex" onclick="setAgent('codex');closeAgentMenu()">
|
|
766
|
-
<span>🟢 Codex</span><span class="agent-menu-check" id="amCodexCheck">✓</span>
|
|
767
|
-
</button>
|
|
768
|
-
<div class="agent-menu-divider" id="amModelDivider"></div>
|
|
769
|
-
<div class="agent-menu-label" id="amModelLabel">Модель Claude</div>
|
|
770
|
-
<button class="agent-menu-item" id="amSonnet46" onclick="setModel('claude-sonnet-4-6');closeAgentMenu()">
|
|
771
|
-
<span>sonnet-4-6 <span style="color:var(--dim);font-size:10px">быстрый</span></span><span class="agent-menu-check" id="amSonnet46Check">✓</span>
|
|
772
|
-
</button>
|
|
773
|
-
<button class="agent-menu-item" id="amOpus45" onclick="setModel('claude-opus-4-5');closeAgentMenu()">
|
|
774
|
-
<span>opus-4-5 <span style="color:var(--dim);font-size:10px">умный</span></span><span class="agent-menu-check" id="amOpus45Check">✓</span>
|
|
775
|
-
</button>
|
|
776
|
-
<button class="agent-menu-item" id="amHaiku35" onclick="setModel('claude-haiku-3-5');closeAgentMenu()">
|
|
777
|
-
<span>haiku-3-5 <span style="color:var(--dim);font-size:10px">дешёвый</span></span><span class="agent-menu-check" id="amHaiku35Check">✓</span>
|
|
778
|
-
</button>
|
|
779
|
-
<div class="agent-menu-divider"></div>
|
|
780
|
-
<button class="agent-menu-item" onclick="reauthAgent();closeAgentMenu()">🔑 Перелогиниться</button>
|
|
781
|
-
</div>
|
|
782
|
-
</div>
|
|
783
723
|
<span id="liveDot" title="Connecting…" style="
|
|
784
724
|
width:8px; height:8px; border-radius:50%;
|
|
785
725
|
background:var(--dim); display:inline-block;
|
|
@@ -814,13 +754,9 @@
|
|
|
814
754
|
<div class="agent-panel-header">
|
|
815
755
|
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
816
756
|
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
817
|
-
<span class="agent-queue-badge" id="agentQueueBadge" style="display:none">📋 <span id="agentQueueCount">0</span> в очереди</span>
|
|
818
|
-
<button class="agent-panel-cancel" id="agentQueueClearBtn" onclick="clearAgentQueue()" title="Очистить очередь" style="display:none">🗑 очередь</button>
|
|
819
757
|
<button class="agent-panel-cancel" id="agentCancelBtn" onclick="cancelAgent()" title="Сбросить состояние агента" style="display:none">⏹ сброс</button>
|
|
820
|
-
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
821
758
|
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
822
759
|
</div>
|
|
823
|
-
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
824
760
|
<div class="agent-terminal" id="agentTerminal"></div>
|
|
825
761
|
</div>
|
|
826
762
|
|
|
@@ -835,208 +771,229 @@ let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string
|
|
|
835
771
|
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
836
772
|
let coverageRunning = false;
|
|
837
773
|
let coverageHasError = false;
|
|
774
|
+
let e2ePlan = null;
|
|
775
|
+
let e2ePlanLoading = false;
|
|
838
776
|
|
|
839
|
-
// ───
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
if (
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
} else {
|
|
852
|
-
btn.textContent = '🧪 Coverage';
|
|
853
|
-
}
|
|
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
|
+
});
|
|
854
789
|
}
|
|
855
790
|
|
|
856
|
-
async function
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
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; }
|
|
860
797
|
}
|
|
861
798
|
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
let activeSessionId = null; // currently viewed tab
|
|
870
|
-
let runningSessionId = null; // tab that is currently receiving output
|
|
871
|
-
const SESSION_MAX = 25;
|
|
872
|
-
const SESSIONS_KEY = 'viberadar_sessions';
|
|
873
|
-
|
|
874
|
-
function _sessionId() {
|
|
875
|
-
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(); }
|
|
876
806
|
}
|
|
877
807
|
|
|
878
|
-
function
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
})
|
|
883
|
-
|
|
884
|
-
|
|
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(); }
|
|
885
815
|
}
|
|
886
816
|
|
|
887
|
-
function
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
if (s.status === 'running') {
|
|
895
|
-
s.status = 'error';
|
|
896
|
-
s.lines.push({ text: '⚡ Прервано (перезагрузка страницы)', isError: true });
|
|
897
|
-
}
|
|
898
|
-
}
|
|
899
|
-
if (consoleSessions.length > 0) {
|
|
900
|
-
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
901
|
-
renderTabs();
|
|
902
|
-
renderActiveSession();
|
|
903
|
-
}
|
|
904
|
-
} 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
|
+
});
|
|
905
824
|
}
|
|
906
825
|
|
|
907
|
-
function
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
914
|
-
renderTabs();
|
|
915
|
-
renderActiveSession();
|
|
916
|
-
saveSessions();
|
|
917
|
-
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
|
+
});
|
|
918
832
|
}
|
|
919
833
|
|
|
920
|
-
function
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
document.getElementById('agentPanelStatus').textContent = statusText;
|
|
930
|
-
}
|
|
931
|
-
renderTabs();
|
|
932
|
-
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);
|
|
933
843
|
}
|
|
934
844
|
|
|
935
|
-
function
|
|
936
|
-
const
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
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>';
|
|
947
856
|
}
|
|
948
857
|
|
|
949
|
-
function
|
|
950
|
-
const
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
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;
|
|
957
914
|
}
|
|
958
|
-
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
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;
|
|
970
936
|
}
|
|
971
|
-
}
|
|
972
937
|
|
|
973
|
-
|
|
974
|
-
const
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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>';
|
|
979
970
|
}
|
|
980
971
|
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
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
|
+
}
|
|
997
987
|
}
|
|
998
988
|
|
|
999
|
-
function
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
//
|
|
1003
|
-
const text = s.lines.map(l => {
|
|
1004
|
-
if (l.text !== undefined) return l.text;
|
|
1005
|
-
if (l.html) {
|
|
1006
|
-
const tmp = document.createElement('div');
|
|
1007
|
-
tmp.innerHTML = l.html;
|
|
1008
|
-
return tmp.innerText;
|
|
1009
|
-
}
|
|
1010
|
-
return '';
|
|
1011
|
-
}).filter(Boolean).join('\n');
|
|
1012
|
-
navigator.clipboard.writeText(text).then(() => {
|
|
1013
|
-
const btn = document.getElementById('agentCopyBtn');
|
|
1014
|
-
if (!btn) return;
|
|
1015
|
-
const prev = btn.textContent;
|
|
1016
|
-
btn.textContent = '✓';
|
|
1017
|
-
btn.classList.add('copied');
|
|
1018
|
-
setTimeout(() => { btn.textContent = prev; btn.classList.remove('copied'); }, 1500);
|
|
1019
|
-
});
|
|
989
|
+
async function runCoverage() {
|
|
990
|
+
if (coverageRunning) return;
|
|
991
|
+
await fetch('/api/run-coverage', { method: 'POST' });
|
|
992
|
+
// SSE events will update state
|
|
1020
993
|
}
|
|
1021
994
|
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
if (!term) return;
|
|
1025
|
-
term.innerHTML = '';
|
|
1026
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1027
|
-
if (!s) return;
|
|
1028
|
-
for (const ln of s.lines) {
|
|
1029
|
-
const el = document.createElement('div');
|
|
1030
|
-
if (ln.html) {
|
|
1031
|
-
el.innerHTML = ln.html;
|
|
1032
|
-
} else {
|
|
1033
|
-
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1034
|
-
el.textContent = ln.text;
|
|
1035
|
-
}
|
|
1036
|
-
term.appendChild(el);
|
|
1037
|
-
}
|
|
1038
|
-
term.scrollTop = term.scrollHeight;
|
|
1039
|
-
}
|
|
995
|
+
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
996
|
+
let agentRunning = false;
|
|
1040
997
|
|
|
1041
998
|
async function setAgent(agent) {
|
|
1042
999
|
await fetch('/api/set-agent', {
|
|
@@ -1047,182 +1004,30 @@ async function setAgent(agent) {
|
|
|
1047
1004
|
// scheduleRescan will fire → data-updated → D.agent updates
|
|
1048
1005
|
}
|
|
1049
1006
|
|
|
1050
|
-
async function setModel(model) {
|
|
1051
|
-
await fetch('/api/set-model', {
|
|
1052
|
-
method: 'POST',
|
|
1053
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1054
|
-
body: JSON.stringify({ model }),
|
|
1055
|
-
});
|
|
1056
|
-
// scheduleRescan will fire → data-updated → D.model updates
|
|
1057
|
-
}
|
|
1058
|
-
|
|
1059
1007
|
function setAgentRunning(val) {
|
|
1060
1008
|
agentRunning = val;
|
|
1061
1009
|
document.getElementById('agentCancelBtn').style.display = val ? 'inline-block' : 'none';
|
|
1062
1010
|
}
|
|
1063
1011
|
|
|
1064
|
-
// Returns array of normalized relative paths affected by a given task
|
|
1065
|
-
function getTaskFilePaths(task, featureKey, filePath) {
|
|
1066
|
-
if ((task === 'write-tests-file' || task === 'fix-tests') && filePath) {
|
|
1067
|
-
return [filePath.replace(/\\/g, '/')];
|
|
1068
|
-
}
|
|
1069
|
-
if ((task === 'write-tests' || task === 'fix-tests-all') && featureKey && D?.modules) {
|
|
1070
|
-
return D.modules
|
|
1071
|
-
.filter(m => m.featureKeys?.includes(featureKey) && m.type !== 'test' && (!m.hasTests || m.testStale))
|
|
1072
|
-
.map(m => m.relativePath.replace(/\\/g, '/'));
|
|
1073
|
-
}
|
|
1074
|
-
return [];
|
|
1075
|
-
}
|
|
1076
|
-
|
|
1077
|
-
// Returns 'running', 'queued', or null for a given relative path
|
|
1078
|
-
function isFileAgentActive(relPath) {
|
|
1079
|
-
const n = relPath.replace(/\\/g, '/');
|
|
1080
|
-
if (agentRunningPaths.has(n)) return 'running';
|
|
1081
|
-
if (agentQueuedPaths.has(n)) return 'queued';
|
|
1082
|
-
return null;
|
|
1083
|
-
}
|
|
1084
|
-
|
|
1085
|
-
function updateQueueBadge(n) {
|
|
1086
|
-
document.getElementById('agentQueueCount').textContent = n;
|
|
1087
|
-
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1088
|
-
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1089
|
-
}
|
|
1090
|
-
|
|
1091
1012
|
async function cancelAgent() {
|
|
1092
1013
|
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1093
1014
|
setAgentRunning(false);
|
|
1094
|
-
updateQueueBadge(0);
|
|
1095
1015
|
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1096
|
-
|
|
1097
|
-
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1098
|
-
updateSessionStatus(runningSessionId, 'error');
|
|
1099
|
-
runningSessionId = null;
|
|
1100
|
-
} else {
|
|
1101
|
-
appendTerminalLine('⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1102
|
-
}
|
|
1103
|
-
}
|
|
1104
|
-
|
|
1105
|
-
async function clearAgentQueue() {
|
|
1106
|
-
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1107
|
-
updateQueueBadge(0);
|
|
1108
|
-
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1109
|
-
}
|
|
1110
|
-
|
|
1111
|
-
// ─── File row more menu ──────────────────────────────────────────────────────
|
|
1112
|
-
let _openFileMenu = null;
|
|
1113
|
-
|
|
1114
|
-
function toggleFileMenu(btn, featureKey, relPath) {
|
|
1115
|
-
const dropdown = btn.nextElementSibling;
|
|
1116
|
-
const isOpen = dropdown.classList.contains('open');
|
|
1117
|
-
// Close any open menu first
|
|
1118
|
-
if (_openFileMenu && _openFileMenu !== dropdown) {
|
|
1119
|
-
_openFileMenu.classList.remove('open');
|
|
1120
|
-
}
|
|
1121
|
-
dropdown.classList.toggle('open', !isOpen);
|
|
1122
|
-
_openFileMenu = isOpen ? null : dropdown;
|
|
1123
|
-
}
|
|
1124
|
-
|
|
1125
|
-
// Close menu on outside click
|
|
1126
|
-
document.addEventListener('click', () => {
|
|
1127
|
-
if (_openFileMenu) { _openFileMenu.classList.remove('open'); _openFileMenu = null; }
|
|
1128
|
-
});
|
|
1129
|
-
|
|
1130
|
-
async function copyPromptForFile(featureKey, relPath, dropdown) {
|
|
1131
|
-
if (dropdown) { dropdown.classList.remove('open'); _openFileMenu = null; }
|
|
1132
|
-
try {
|
|
1133
|
-
const res = await fetch('/api/get-prompt', {
|
|
1134
|
-
method: 'POST',
|
|
1135
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1136
|
-
body: JSON.stringify({ task: 'write-tests-file', featureKey, filePath: relPath }),
|
|
1137
|
-
});
|
|
1138
|
-
const { prompt, error } = await res.json();
|
|
1139
|
-
if (error || !prompt) { alert('Не удалось получить промпт: ' + (error || 'пустой')); return; }
|
|
1140
|
-
await navigator.clipboard.writeText(prompt);
|
|
1141
|
-
// Brief visual feedback in terminal
|
|
1142
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
1143
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
1144
|
-
appendTerminalLine(`📋 Промпт для "${relPath.split('/').pop()}" скопирован в буфер обмена`, false);
|
|
1145
|
-
appendTerminalLine(' Вставь его в другой агент (Claude, Codex, ChatGPT) и запусти вручную.', false);
|
|
1146
|
-
} catch (e) {
|
|
1147
|
-
alert('Ошибка копирования: ' + e.message);
|
|
1148
|
-
}
|
|
1016
|
+
appendTerminalLine('⏹ Состояние агента сброшено', false);
|
|
1149
1017
|
}
|
|
1150
1018
|
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
{ id: 'claude-opus-4-5', checkId: 'amOpus45Check' },
|
|
1155
|
-
{ id: 'claude-haiku-3-5', checkId: 'amHaiku35Check' },
|
|
1156
|
-
];
|
|
1157
|
-
|
|
1158
|
-
function updateAgentBtn() {
|
|
1159
|
-
const btn = document.getElementById('agentBtn');
|
|
1160
|
-
if (!D) return;
|
|
1161
|
-
const a = D.agent;
|
|
1162
|
-
const m = D.model;
|
|
1163
|
-
// Button label: agent + model shortname
|
|
1164
|
-
let label = a === 'claude' ? '🤖 Claude' : a === 'codex' ? '🤖 Codex' : '🤖 не выбран';
|
|
1165
|
-
if (a === 'claude' && m) {
|
|
1166
|
-
// Show short model name: "sonnet-4-6", "opus-4-5", "haiku-3-5"
|
|
1167
|
-
const short = m.replace('claude-', '');
|
|
1168
|
-
label += ` · ${short}`;
|
|
1169
|
-
}
|
|
1170
|
-
btn.textContent = label;
|
|
1171
|
-
// Update agent checkmarks
|
|
1172
|
-
document.getElementById('amClaudeCheck').classList.toggle('active', a === 'claude');
|
|
1173
|
-
document.getElementById('amCodexCheck').classList.toggle('active', a === 'codex');
|
|
1174
|
-
// Show/hide model section (only for claude)
|
|
1175
|
-
const isClaude = a === 'claude';
|
|
1176
|
-
document.getElementById('amModelDivider').style.display = isClaude ? '' : 'none';
|
|
1177
|
-
document.getElementById('amModelLabel').style.display = isClaude ? '' : 'none';
|
|
1178
|
-
CLAUDE_MODELS.forEach(({ id, checkId }) => {
|
|
1179
|
-
const el = document.getElementById(id);
|
|
1180
|
-
if (el) el.style.display = isClaude ? '' : 'none';
|
|
1181
|
-
document.getElementById(checkId).classList.toggle('active', m === id);
|
|
1182
|
-
});
|
|
1183
|
-
}
|
|
1184
|
-
|
|
1185
|
-
function toggleAgentMenu() {
|
|
1186
|
-
const menu = document.getElementById('agentMenu');
|
|
1187
|
-
const isOpen = menu.classList.contains('open');
|
|
1188
|
-
menu.classList.toggle('open');
|
|
1189
|
-
if (!isOpen) {
|
|
1190
|
-
// Close on click outside
|
|
1191
|
-
setTimeout(() => {
|
|
1192
|
-
document.addEventListener('click', closeAgentMenuOnOutside, { once: true });
|
|
1193
|
-
}, 0);
|
|
1194
|
-
}
|
|
1195
|
-
}
|
|
1196
|
-
function closeAgentMenu() {
|
|
1197
|
-
document.getElementById('agentMenu').classList.remove('open');
|
|
1198
|
-
}
|
|
1199
|
-
function closeAgentMenuOnOutside(e) {
|
|
1200
|
-
if (!e.target.closest('#agentMenu') && !e.target.closest('#agentBtn')) {
|
|
1201
|
-
closeAgentMenu();
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
async function reauthAgent() {
|
|
1206
|
-
const agent = D?.agent;
|
|
1207
|
-
if (!agent) {
|
|
1208
|
-
appendTerminalLine('⚠ Сначала выбери агента (Claude или Codex)', true);
|
|
1019
|
+
async function runAgentTask(task, featureKey, filePath) {
|
|
1020
|
+
if (agentRunning) {
|
|
1021
|
+
// Show panel so user sees cancel button
|
|
1209
1022
|
document.getElementById('agentPanel').classList.add('open');
|
|
1210
1023
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1024
|
+
appendTerminalLine('⚠️ Агент уже запущен. Нажми ⏹ сброс чтобы отменить.', true);
|
|
1211
1025
|
return;
|
|
1212
1026
|
}
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
document.getElementById('agentPanelTitle').textContent = '🔑 Перелогинивание';
|
|
1216
|
-
document.getElementById('agentPanelStatus').textContent = 'выполняю…';
|
|
1217
|
-
await fetch('/api/agent-reauth', { method: 'POST' });
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
async function runAgentTask(task, featureKey, filePath) {
|
|
1027
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
1028
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1221
1029
|
document.getElementById('agentPanel').classList.add('open');
|
|
1222
1030
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1223
|
-
if (!agentRunning) {
|
|
1224
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1225
|
-
}
|
|
1226
1031
|
await fetch('/api/run-agent', {
|
|
1227
1032
|
method: 'POST',
|
|
1228
1033
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1231,9 +1036,11 @@ async function runAgentTask(task, featureKey, filePath) {
|
|
|
1231
1036
|
}
|
|
1232
1037
|
|
|
1233
1038
|
async function runTests(featureKey, testType) {
|
|
1039
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
1040
|
+
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
1041
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1234
1042
|
document.getElementById('agentPanel').classList.add('open');
|
|
1235
1043
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1236
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1237
1044
|
await fetch('/api/run-tests', {
|
|
1238
1045
|
method: 'POST',
|
|
1239
1046
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1254,11 +1061,12 @@ function toggleAgentPanel() {
|
|
|
1254
1061
|
}
|
|
1255
1062
|
|
|
1256
1063
|
function appendTerminalLine(line, isError, isDim) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
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;
|
|
1262
1070
|
}
|
|
1263
1071
|
|
|
1264
1072
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
@@ -1305,7 +1113,6 @@ async function init() {
|
|
|
1305
1113
|
coverageHasError = status.coverageError ?? false;
|
|
1306
1114
|
}
|
|
1307
1115
|
updateCovBtn();
|
|
1308
|
-
updateAgentBtn();
|
|
1309
1116
|
|
|
1310
1117
|
document.getElementById('projectName').textContent = D.projectName;
|
|
1311
1118
|
document.getElementById('scannedAt').textContent =
|
|
@@ -1558,39 +1365,31 @@ function renderFeatureCards(c) {
|
|
|
1558
1365
|
}
|
|
1559
1366
|
|
|
1560
1367
|
function testTypeCard(type, label, icon, color, count, active, featureKey, failedCount = 0) {
|
|
1561
|
-
const empty
|
|
1368
|
+
const empty = count === 0 && type !== 'source';
|
|
1562
1369
|
const hasFailed = failedCount > 0 && type !== 'source';
|
|
1563
|
-
const
|
|
1564
|
-
const
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
const failedBadge = hasFailed
|
|
1568
|
-
? `<span class="tt-failed-badge">❌ ${failedCount}</span>`
|
|
1569
|
-
: '';
|
|
1570
|
-
|
|
1370
|
+
const hasE2ePlan = type === 'e2e' && D.e2ePlansExist && D.e2ePlansExist.includes(featureKey);
|
|
1371
|
+
const subLabel = empty
|
|
1372
|
+
? (type === 'e2e' && hasE2ePlan ? '📋 план создан' : 'нет тестов')
|
|
1373
|
+
: (type === 'source' ? 'код приложения' : pluralFiles(count));
|
|
1571
1374
|
const runBtn = !empty && type !== 'source' && featureKey
|
|
1572
|
-
? `<button class="tt-run-btn" onclick="event.stopPropagation()
|
|
1573
|
-
: '';
|
|
1574
|
-
const fixAllBtn = hasFailed && D.agent && featureKey
|
|
1575
|
-
? `<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>`
|
|
1576
1376
|
: '';
|
|
1577
|
-
const
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
? `<div class="tt-card-actions">${runBtn}${fixAllBtn}${writeBtn}</div>`
|
|
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
|
-
|
|
1382
|
+
// E2E cards should always be clickable (to see/create plan)
|
|
1383
|
+
const forceClickable = type === 'e2e';
|
|
1584
1384
|
return `
|
|
1585
|
-
<div class="test-type-card${empty ? ' tt-empty' : ''}${active ? ' tt-active' : ''}${hasFailed ? ' tt-failed' : ''}"
|
|
1586
|
-
data-testtype="${type}" style="border-
|
|
1587
|
-
<div class="tt-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
</div>
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
${actions}
|
|
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}
|
|
1594
1393
|
</div>`;
|
|
1595
1394
|
}
|
|
1596
1395
|
|
|
@@ -1598,6 +1397,12 @@ function renderFeatureDetail(c) {
|
|
|
1598
1397
|
const feat = D.features.find(f => f.key === drillFeatureKey);
|
|
1599
1398
|
if (!feat) { backToFeatures(); return; }
|
|
1600
1399
|
|
|
1400
|
+
// E2E plan view override
|
|
1401
|
+
if (drillTestType === 'e2e') {
|
|
1402
|
+
renderE2ePlanView(c, drillFeatureKey);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1601
1406
|
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
1602
1407
|
const src = mods.filter(m => m.type !== 'test');
|
|
1603
1408
|
const tst = mods.filter(m => m.type === 'test');
|
|
@@ -1665,11 +1470,14 @@ function renderFeatureDetail(c) {
|
|
|
1665
1470
|
</div>`;
|
|
1666
1471
|
|
|
1667
1472
|
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
1668
|
-
card.onclick = () => {
|
|
1473
|
+
card.onclick = async () => {
|
|
1669
1474
|
const type = card.dataset.testtype;
|
|
1670
1475
|
drillTestType = (type === 'source') ? null : type; // 'source' tab = null state
|
|
1671
1476
|
activePanelKey = null;
|
|
1672
1477
|
document.getElementById('panel').classList.remove('open');
|
|
1478
|
+
if (type === 'e2e') {
|
|
1479
|
+
await loadE2ePlan(drillFeatureKey);
|
|
1480
|
+
}
|
|
1673
1481
|
renderContent();
|
|
1674
1482
|
};
|
|
1675
1483
|
});
|
|
@@ -1824,16 +1632,12 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1824
1632
|
const dir = parts.slice(0, -1).join('/');
|
|
1825
1633
|
|
|
1826
1634
|
// Test errors from last run
|
|
1827
|
-
const testErr
|
|
1828
|
-
const
|
|
1829
|
-
const icon = agentState
|
|
1830
|
-
? `<span class="file-agent-spinner ${agentState}" title="${agentState === 'running' ? 'Агент работает…' : 'В очереди…'}"></span>`
|
|
1831
|
-
: (testErr ? '❌' : isTest ? '🧪' : (m.testStale ? '⚠️' : m.hasTests ? '✅' : '⬜'));
|
|
1635
|
+
const testErr = isTest && D.testErrors && D.testErrors[relPath];
|
|
1636
|
+
const icon = testErr ? '❌' : isTest ? '🧪' : (m.testStale ? '⚠️' : m.hasTests ? '✅' : '⬜');
|
|
1832
1637
|
const isActive = activePanelKey === m.id;
|
|
1833
1638
|
|
|
1834
1639
|
// Write-test button for source files
|
|
1835
|
-
|
|
1836
|
-
const showAgentBtn = D.agent && !isTest && featureKey && (!m.hasTests || m.testStale);
|
|
1640
|
+
const showAgentBtn = D.agent && !isTest && !m.isInfra && featureKey && (!m.hasTests || m.testStale);
|
|
1837
1641
|
const agentBtnLabel = m.testStale ? '↻ обновить тест' : '✍ написать тест';
|
|
1838
1642
|
const agentBtnTitle = m.testStale ? 'Файл изменился — обновить тесты' : 'Написать тест для этого файла';
|
|
1839
1643
|
const agentBtn = showAgentBtn
|
|
@@ -1864,20 +1668,6 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1864
1668
|
</div>`
|
|
1865
1669
|
: '';
|
|
1866
1670
|
|
|
1867
|
-
// "⋯" more menu — always show for source files in feature context
|
|
1868
|
-
const moreBtn = !isTest && featureKey
|
|
1869
|
-
? `<div class="file-row-more-wrap">
|
|
1870
|
-
<button class="file-row-more-btn" title="Ещё"
|
|
1871
|
-
onclick="event.stopPropagation();toggleFileMenu(this,'${featureKey}','${relPath}')">⋯</button>
|
|
1872
|
-
<div class="file-row-more-dropdown">
|
|
1873
|
-
<button class="file-row-more-item"
|
|
1874
|
-
onclick="event.stopPropagation();copyPromptForFile('${featureKey}','${relPath}',this.closest('.file-row-more-dropdown'))">
|
|
1875
|
-
📋 Скопировать промпт для агента
|
|
1876
|
-
</button>
|
|
1877
|
-
</div>
|
|
1878
|
-
</div>`
|
|
1879
|
-
: '';
|
|
1880
|
-
|
|
1881
1671
|
return `
|
|
1882
1672
|
<div class="file-row${isActive ? ' active' : ''}${testErr ? ' has-errors' : ''}" data-id="${m.id}">
|
|
1883
1673
|
<span class="file-row-icon">${icon}</span>
|
|
@@ -1885,7 +1675,6 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1885
1675
|
${errBadge}
|
|
1886
1676
|
${agentBtn}
|
|
1887
1677
|
${fixBtn}
|
|
1888
|
-
${moreBtn}
|
|
1889
1678
|
<span class="file-row-dir">${dir}</span>
|
|
1890
1679
|
</div>
|
|
1891
1680
|
${errHtml}`;
|
|
@@ -1981,11 +1770,7 @@ function fileItem(m, isTest = false) {
|
|
|
1981
1770
|
const parts = m.relativePath.replace(/\\/g, '/').split('/');
|
|
1982
1771
|
const name = parts[parts.length - 1];
|
|
1983
1772
|
const dir = parts.slice(0, -1).join('/');
|
|
1984
|
-
const
|
|
1985
|
-
const agentStateItem = !isTest ? isFileAgentActive(relPathNorm) : null;
|
|
1986
|
-
const icon = agentStateItem
|
|
1987
|
-
? `<span class="file-agent-spinner ${agentStateItem}" title="${agentStateItem === 'running' ? 'Агент работает…' : 'В очереди…'}"></span>`
|
|
1988
|
-
: (isTest ? '🧪' : (m.hasTests ? '✅' : '⬜'));
|
|
1773
|
+
const icon = isTest ? '🧪' : (m.hasTests ? '✅' : '⬜');
|
|
1989
1774
|
return `
|
|
1990
1775
|
<div class="file-item">
|
|
1991
1776
|
<span class="file-item-icon">${icon}</span>
|
|
@@ -2183,7 +1968,6 @@ async function refreshData() {
|
|
|
2183
1968
|
renderStats();
|
|
2184
1969
|
renderSidebar();
|
|
2185
1970
|
renderContent();
|
|
2186
|
-
updateAgentBtn();
|
|
2187
1971
|
|
|
2188
1972
|
// Re-render drill-down or re-open panel
|
|
2189
1973
|
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
@@ -2237,69 +2021,31 @@ function connectSSE() {
|
|
|
2237
2021
|
setTimeout(() => { coverageHasError = false; updateCovBtn(); }, 8000);
|
|
2238
2022
|
});
|
|
2239
2023
|
|
|
2240
|
-
es.addEventListener('agent-queued', (e) => {
|
|
2241
|
-
const { queueLength, title, task, featureKey, filePath } = JSON.parse(e.data);
|
|
2242
|
-
updateQueueBadge(queueLength);
|
|
2243
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
2244
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
2245
|
-
// Track queued paths for spinner
|
|
2246
|
-
getTaskFilePaths(task, featureKey, filePath).forEach(p => agentQueuedPaths.add(p));
|
|
2247
|
-
renderContent();
|
|
2248
|
-
// Append queue notification to the currently running session (or active)
|
|
2249
|
-
const targetId = runningSessionId || activeSessionId;
|
|
2250
|
-
if (targetId) {
|
|
2251
|
-
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
2252
|
-
}
|
|
2253
|
-
});
|
|
2254
|
-
|
|
2255
2024
|
es.addEventListener('agent-started', (e) => {
|
|
2256
2025
|
setAgentRunning(true);
|
|
2257
|
-
const { title
|
|
2258
|
-
updateQueueBadge(queueLength);
|
|
2259
|
-
// Move paths from queued → running (current task)
|
|
2260
|
-
const startedPaths = getTaskFilePaths(task, featureKey, filePath);
|
|
2261
|
-
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
2262
|
-
renderContent();
|
|
2263
|
-
// Close previous session (queue case: agent-done not fired between tasks)
|
|
2264
|
-
if (runningSessionId) {
|
|
2265
|
-
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
2266
|
-
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
2267
|
-
}
|
|
2268
|
-
const id = createSession(title);
|
|
2269
|
-
runningSessionId = id;
|
|
2026
|
+
const { title } = JSON.parse(e.data);
|
|
2270
2027
|
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
2271
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 = '';
|
|
2272
2032
|
});
|
|
2273
2033
|
|
|
2274
2034
|
es.addEventListener('agent-output', (e) => {
|
|
2275
2035
|
const { line, isError, isDim } = JSON.parse(e.data);
|
|
2276
|
-
|
|
2277
|
-
|
|
2278
|
-
if (activeSessionId === runningSessionId) {
|
|
2279
|
-
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
2280
|
-
}
|
|
2281
|
-
} else {
|
|
2282
|
-
appendTerminalLine(line, !!isError, !!isDim);
|
|
2283
|
-
}
|
|
2036
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
2037
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
2284
2038
|
});
|
|
2285
2039
|
|
|
2286
2040
|
es.addEventListener('agent-done', () => {
|
|
2287
2041
|
setAgentRunning(false);
|
|
2288
|
-
|
|
2289
|
-
agentRunningPaths.clear();
|
|
2290
|
-
agentQueuedPaths.clear();
|
|
2291
|
-
if (runningSessionId) {
|
|
2292
|
-
updateSessionStatus(runningSessionId, 'ok');
|
|
2293
|
-
if (activeSessionId === runningSessionId) {
|
|
2294
|
-
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
2295
|
-
}
|
|
2296
|
-
runningSessionId = null;
|
|
2297
|
-
}
|
|
2042
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
2298
2043
|
renderContent();
|
|
2299
2044
|
});
|
|
2300
2045
|
|
|
2301
2046
|
es.addEventListener('agent-summary', (e) => {
|
|
2302
2047
|
const { passed, failed, files } = JSON.parse(e.data);
|
|
2048
|
+
const term = document.getElementById('agentTerminal');
|
|
2303
2049
|
const allOk = failed === 0;
|
|
2304
2050
|
const box = document.createElement('div');
|
|
2305
2051
|
box.style.cssText = `
|
|
@@ -2316,82 +2062,66 @@ function connectSSE() {
|
|
|
2316
2062
|
</div>
|
|
2317
2063
|
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
2318
2064
|
`;
|
|
2319
|
-
|
|
2320
|
-
|
|
2321
|
-
appendToSession(targetId, box);
|
|
2322
|
-
// Mark session status from test results immediately
|
|
2323
|
-
if (runningSessionId === targetId) {
|
|
2324
|
-
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2065
|
+
term.appendChild(box);
|
|
2066
|
+
term.scrollTop = term.scrollHeight;
|
|
2327
2067
|
});
|
|
2328
2068
|
|
|
2329
2069
|
es.addEventListener('agent-error', (e) => {
|
|
2330
2070
|
setAgentRunning(false);
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
const { message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
2335
|
-
|
|
2336
|
-
// Build error node (plain text or auth/install box)
|
|
2337
|
-
let node = null;
|
|
2338
|
-
if (authRequired || notInstalled) {
|
|
2339
|
-
node = document.createElement('div');
|
|
2340
|
-
node.style.cssText = 'margin:10px 0 4px;padding:10px 14px;border-radius:8px;border:1px solid var(--red);background:#2a0d0d;font-family:inherit;';
|
|
2341
|
-
node.innerHTML = `
|
|
2342
|
-
<div style="font-size:13px;font-weight:700;color:var(--red)">❌ ${message || 'Ошибка агента'}</div>
|
|
2343
|
-
${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>` : ''}
|
|
2344
|
-
${notInstalled ? `<div style="margin-top:6px;font-size:11px;color:var(--dim)">После установки перезапусти viberadar</div>` : ''}
|
|
2345
|
-
`;
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
// If no session exists yet (startup check fires before any run), create one
|
|
2349
|
-
const targetId = runningSessionId || (() => {
|
|
2350
|
-
if (authRequired || notInstalled) {
|
|
2351
|
-
const id = createSession('⚠️ Проверка агента', 'error');
|
|
2352
|
-
return id;
|
|
2353
|
-
}
|
|
2354
|
-
return activeSessionId;
|
|
2355
|
-
})();
|
|
2356
|
-
|
|
2357
|
-
if (targetId) {
|
|
2358
|
-
if (node) {
|
|
2359
|
-
appendToSession(targetId, node);
|
|
2360
|
-
} else {
|
|
2361
|
-
appendToSession(targetId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
2362
|
-
}
|
|
2363
|
-
updateSessionStatus(targetId, 'error');
|
|
2364
|
-
if (activeSessionId === targetId) {
|
|
2365
|
-
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
2366
|
-
}
|
|
2367
|
-
}
|
|
2368
|
-
if (runningSessionId) runningSessionId = null;
|
|
2071
|
+
const { message } = JSON.parse(e.data);
|
|
2072
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
2073
|
+
appendTerminalLine('❌ ' + (message || 'Ошибка агента'), true);
|
|
2369
2074
|
});
|
|
2370
2075
|
|
|
2371
2076
|
es.addEventListener('tests-started', (e) => {
|
|
2372
2077
|
const { testType, count } = JSON.parse(e.data);
|
|
2373
|
-
const id = createSession(`🧪 ${testType}`);
|
|
2374
|
-
runningSessionId = id;
|
|
2375
|
-
appendToSession(id, `Запускаю ${testType} тесты (${count} файлов)…`);
|
|
2376
2078
|
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
2377
2079
|
document.getElementById('agentPanelStatus').textContent = `запускаю ${count} файлов…`;
|
|
2378
2080
|
});
|
|
2379
2081
|
|
|
2380
2082
|
es.addEventListener('tests-done', (e) => {
|
|
2381
2083
|
const { passed, failed, testErrors } = JSON.parse(e.data);
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
if (activeSessionId === runningSessionId) {
|
|
2386
|
-
document.getElementById('agentPanelStatus').textContent =
|
|
2387
|
-
failed === 0 ? `✅ ${passed} passed` : `⚠️ ${passed} passed, ${failed} failed`;
|
|
2388
|
-
}
|
|
2389
|
-
runningSessionId = null;
|
|
2390
|
-
}
|
|
2084
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
2085
|
+
failed === 0 ? `✅ ${passed} passed` : `⚠️ ${passed} passed, ${failed} failed`;
|
|
2086
|
+
// Apply testErrors directly from event — no separate fetch needed
|
|
2391
2087
|
D.testErrors = testErrors || {};
|
|
2392
2088
|
renderContent();
|
|
2393
2089
|
});
|
|
2394
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
|
+
|
|
2395
2125
|
es.onerror = () => {
|
|
2396
2126
|
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
2397
2127
|
es.close();
|
|
@@ -2399,15 +2129,7 @@ function connectSSE() {
|
|
|
2399
2129
|
};
|
|
2400
2130
|
}
|
|
2401
2131
|
|
|
2402
|
-
init().then(() =>
|
|
2403
|
-
restoreSessions();
|
|
2404
|
-
connectSSE();
|
|
2405
|
-
// Sync content padding with terminal panel open state automatically
|
|
2406
|
-
new MutationObserver(() => {
|
|
2407
|
-
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
2408
|
-
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
2409
|
-
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
2410
|
-
});
|
|
2132
|
+
init().then(() => connectSSE());
|
|
2411
2133
|
</script>
|
|
2412
2134
|
</body>
|
|
2413
2135
|
</html>
|