viberadar 0.3.46 → 0.3.48
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/server/index.d.ts.map +1 -1
- package/dist/server/index.js +441 -360
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +898 -395
- package/package.json +43 -43
package/dist/ui/dashboard.html
CHANGED
|
@@ -83,6 +83,68 @@
|
|
|
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
|
+
|
|
86
148
|
/* ── Stats bar ───────────────────────────────────────────────────────────── */
|
|
87
149
|
.stats-bar {
|
|
88
150
|
display: flex;
|
|
@@ -172,7 +234,8 @@
|
|
|
172
234
|
.type-count { margin-left: auto; font-size: 11px; color: var(--dim); }
|
|
173
235
|
|
|
174
236
|
/* ── Content area ────────────────────────────────────────────────────────── */
|
|
175
|
-
.content { flex: 1; overflow-y: auto; padding: 18px 20px; }
|
|
237
|
+
.content { flex: 1; overflow-y: auto; padding: 18px 20px; transition: padding-bottom 0.25s ease; }
|
|
238
|
+
.content.panel-open { padding-bottom: 300px; }
|
|
176
239
|
|
|
177
240
|
/* ── Feature cards ───────────────────────────────────────────────────────── */
|
|
178
241
|
.features-grid {
|
|
@@ -420,56 +483,48 @@
|
|
|
420
483
|
/* ── Test-type cards inside feature detail ───────────────────────────────── */
|
|
421
484
|
.test-type-grid {
|
|
422
485
|
display: grid;
|
|
423
|
-
grid-template-columns: repeat(
|
|
486
|
+
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
|
424
487
|
gap: 10px;
|
|
425
|
-
margin-bottom:
|
|
488
|
+
margin-bottom: 18px;
|
|
426
489
|
}
|
|
427
490
|
.test-type-card {
|
|
491
|
+
display: flex;
|
|
492
|
+
flex-direction: column;
|
|
428
493
|
background: var(--bg-card);
|
|
429
494
|
border: 1px solid var(--border);
|
|
495
|
+
border-top: 3px solid transparent;
|
|
430
496
|
border-radius: 8px;
|
|
431
|
-
padding: 14px
|
|
497
|
+
padding: 12px 14px 10px;
|
|
432
498
|
cursor: pointer;
|
|
433
499
|
transition: background 0.15s, border-color 0.15s;
|
|
434
|
-
|
|
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;
|
|
500
|
+
min-width: 0;
|
|
457
501
|
}
|
|
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; }
|
|
458
505
|
.test-type-card.tt-failed {
|
|
459
506
|
border-color: var(--red) !important;
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
507
|
+
background: rgba(248, 81, 73, 0.05);
|
|
508
|
+
}
|
|
509
|
+
.tt-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 8px; }
|
|
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; }
|
|
463
515
|
.tt-run-btn {
|
|
464
|
-
|
|
465
|
-
padding: 3px 0;
|
|
516
|
+
width: 100%; padding: 5px 10px;
|
|
466
517
|
background: transparent; border: 1px solid var(--border);
|
|
467
|
-
border-radius:
|
|
518
|
+
border-radius: 5px; color: var(--muted); font-size: 11px;
|
|
468
519
|
cursor: pointer; text-align: center;
|
|
469
520
|
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
470
521
|
}
|
|
471
522
|
.tt-run-btn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--dim); }
|
|
472
523
|
.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); }
|
|
473
528
|
.file-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
474
529
|
.file-row {
|
|
475
530
|
display: flex;
|
|
@@ -521,6 +576,10 @@
|
|
|
521
576
|
background: rgba(248, 81, 73, 0.07);
|
|
522
577
|
}
|
|
523
578
|
.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); } }
|
|
524
583
|
.file-row-errors {
|
|
525
584
|
padding: 4px 10px 6px 32px;
|
|
526
585
|
display: flex; flex-direction: column; gap: 3px;
|
|
@@ -602,11 +661,74 @@
|
|
|
602
661
|
font-size: 14px; padding: 3px 6px; border-radius: 4px; line-height: 1;
|
|
603
662
|
}
|
|
604
663
|
.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); }
|
|
605
670
|
.agent-panel-cancel {
|
|
606
671
|
background: none; border: 1px solid var(--yellow); color: var(--yellow);
|
|
607
672
|
cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
608
673
|
}
|
|
609
674
|
.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); }
|
|
610
732
|
.agent-terminal {
|
|
611
733
|
flex: 1;
|
|
612
734
|
overflow-y: auto;
|
|
@@ -619,96 +741,50 @@
|
|
|
619
741
|
.agent-line.err { color: var(--red); }
|
|
620
742
|
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
621
743
|
|
|
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
|
-
|
|
709
744
|
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
710
745
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
711
746
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
747
|
+
/* ─── E2E Plan UI ─────────────────────────────────────────────────── */
|
|
748
|
+
.e2e-plan-container { padding: 16px 0; }
|
|
749
|
+
.e2e-batch-bar { display:flex; gap:8px; flex-wrap:wrap; margin-bottom:16px; align-items:center; }
|
|
750
|
+
.e2e-stats { font-size:12px; color:var(--dim); margin-left:auto; }
|
|
751
|
+
.e2e-btn { padding:5px 12px; border-radius:6px; border:1px solid var(--border); background:var(--surface2); color:var(--text); font-size:12px; cursor:pointer; transition:background 0.15s; }
|
|
752
|
+
.e2e-btn:hover { background:var(--surface3); }
|
|
753
|
+
.e2e-btn-approve { border-color:#2ea043; color:#2ea043; }
|
|
754
|
+
.e2e-btn-approve:hover { background:rgba(46,160,67,0.15); }
|
|
755
|
+
.e2e-btn-reject { border-color:#f85149; color:#f85149; }
|
|
756
|
+
.e2e-btn-reject:hover { background:rgba(248,81,73,0.15); }
|
|
757
|
+
.e2e-btn-primary { background:#1f6feb; border-color:#1f6feb; color:#fff; }
|
|
758
|
+
.e2e-btn-primary:hover { background:#388bfd; }
|
|
759
|
+
.e2e-btn-secondary { background:var(--surface3); }
|
|
760
|
+
.e2e-case { border:1px solid var(--border); border-radius:8px; padding:14px; margin-bottom:10px; transition:border-color 0.2s; }
|
|
761
|
+
.e2e-case.approved { border-color:#2ea043; background:rgba(46,160,67,0.05); }
|
|
762
|
+
.e2e-case.rejected { border-color:#f85149; background:rgba(248,81,73,0.05); opacity:0.6; }
|
|
763
|
+
.e2e-case.written { border-color:#58a6ff; background:rgba(88,166,255,0.05); }
|
|
764
|
+
.e2e-case.passed { border-color:#2ea043; background:rgba(46,160,67,0.08); }
|
|
765
|
+
.e2e-case.failed { border-color:#f85149; background:rgba(248,81,73,0.08); }
|
|
766
|
+
.e2e-case-header { display:flex; align-items:flex-start; gap:10px; margin-bottom:8px; }
|
|
767
|
+
.e2e-case-name { font-weight:600; font-size:13px; flex:1; }
|
|
768
|
+
.e2e-status-badge { font-size:11px; padding:2px 8px; border-radius:10px; white-space:nowrap; }
|
|
769
|
+
.e2e-status-badge.pending { background:var(--surface3); color:var(--dim); }
|
|
770
|
+
.e2e-status-badge.approved { background:rgba(46,160,67,0.2); color:#2ea043; }
|
|
771
|
+
.e2e-status-badge.rejected { background:rgba(248,81,73,0.2); color:#f85149; }
|
|
772
|
+
.e2e-status-badge.written { background:rgba(88,166,255,0.2); color:#58a6ff; }
|
|
773
|
+
.e2e-status-badge.passed { background:rgba(46,160,67,0.2); color:#2ea043; }
|
|
774
|
+
.e2e-status-badge.failed { background:rgba(248,81,73,0.2); color:#f85149; }
|
|
775
|
+
.e2e-case-desc { font-size:12px; color:var(--dim); margin-bottom:8px; }
|
|
776
|
+
.e2e-steps { font-size:12px; color:var(--text); margin:4px 0; }
|
|
777
|
+
.e2e-steps ol { margin:4px 0 0 16px; padding:0; }
|
|
778
|
+
.e2e-steps li { margin-bottom:2px; }
|
|
779
|
+
.e2e-expected { font-size:12px; color:var(--dim); margin-top:4px; }
|
|
780
|
+
.e2e-case-actions { display:flex; gap:6px; margin-top:10px; }
|
|
781
|
+
.e2e-error { font-size:11px; color:#f85149; margin-top:6px; font-family:monospace; }
|
|
782
|
+
.e2e-screenshot-strip { display:flex; gap:8px; flex-wrap:wrap; margin-top:8px; }
|
|
783
|
+
.e2e-screenshot { width:80px; height:60px; object-fit:cover; border-radius:4px; border:1px solid var(--border); cursor:pointer; transition:opacity 0.15s; }
|
|
784
|
+
.e2e-screenshot:hover { opacity:0.8; }
|
|
785
|
+
.e2e-screenshot-modal { position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:9999; display:flex; align-items:center; justify-content:center; cursor:pointer; }
|
|
786
|
+
.e2e-screenshot-modal img { max-width:90vw; max-height:90vh; border-radius:8px; }
|
|
787
|
+
.e2e-plan-indicator { font-size:10px; color:#d2a8ff; margin-left:4px; }
|
|
712
788
|
</style>
|
|
713
789
|
</head>
|
|
714
790
|
<body>
|
|
@@ -720,6 +796,31 @@
|
|
|
720
796
|
<span class="header-time" id="scannedAt"></span>
|
|
721
797
|
<button id="covBtn" onclick="runCoverage()" title="Запустить тесты с coverage">🧪 Coverage</button>
|
|
722
798
|
<button id="termBtn" onclick="toggleAgentPanel()" title="Показать/скрыть терминал агента">📟 Terminal</button>
|
|
799
|
+
<div style="position:relative">
|
|
800
|
+
<button id="agentBtn" onclick="toggleAgentMenu()" title="Настройки агента">🤖 —</button>
|
|
801
|
+
<div id="agentMenu" class="agent-menu">
|
|
802
|
+
<div class="agent-menu-label">AI Агент</div>
|
|
803
|
+
<button class="agent-menu-item" id="amClaude" onclick="setAgent('claude');closeAgentMenu()">
|
|
804
|
+
<span>⚡ Claude Code</span><span class="agent-menu-check" id="amClaudeCheck">✓</span>
|
|
805
|
+
</button>
|
|
806
|
+
<button class="agent-menu-item" id="amCodex" onclick="setAgent('codex');closeAgentMenu()">
|
|
807
|
+
<span>🟢 Codex</span><span class="agent-menu-check" id="amCodexCheck">✓</span>
|
|
808
|
+
</button>
|
|
809
|
+
<div class="agent-menu-divider" id="amModelDivider"></div>
|
|
810
|
+
<div class="agent-menu-label" id="amModelLabel">Модель Claude</div>
|
|
811
|
+
<button class="agent-menu-item" id="amSonnet46" onclick="setModel('claude-sonnet-4-6');closeAgentMenu()">
|
|
812
|
+
<span>sonnet-4-6 <span style="color:var(--dim);font-size:10px">быстрый</span></span><span class="agent-menu-check" id="amSonnet46Check">✓</span>
|
|
813
|
+
</button>
|
|
814
|
+
<button class="agent-menu-item" id="amOpus45" onclick="setModel('claude-opus-4-5');closeAgentMenu()">
|
|
815
|
+
<span>opus-4-5 <span style="color:var(--dim);font-size:10px">умный</span></span><span class="agent-menu-check" id="amOpus45Check">✓</span>
|
|
816
|
+
</button>
|
|
817
|
+
<button class="agent-menu-item" id="amHaiku35" onclick="setModel('claude-haiku-3-5');closeAgentMenu()">
|
|
818
|
+
<span>haiku-3-5 <span style="color:var(--dim);font-size:10px">дешёвый</span></span><span class="agent-menu-check" id="amHaiku35Check">✓</span>
|
|
819
|
+
</button>
|
|
820
|
+
<div class="agent-menu-divider"></div>
|
|
821
|
+
<button class="agent-menu-item" onclick="reauthAgent();closeAgentMenu()">🔑 Перелогиниться</button>
|
|
822
|
+
</div>
|
|
823
|
+
</div>
|
|
723
824
|
<span id="liveDot" title="Connecting…" style="
|
|
724
825
|
width:8px; height:8px; border-radius:50%;
|
|
725
826
|
background:var(--dim); display:inline-block;
|
|
@@ -754,9 +855,13 @@
|
|
|
754
855
|
<div class="agent-panel-header">
|
|
755
856
|
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
756
857
|
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
858
|
+
<span class="agent-queue-badge" id="agentQueueBadge" style="display:none">📋 <span id="agentQueueCount">0</span> в очереди</span>
|
|
859
|
+
<button class="agent-panel-cancel" id="agentQueueClearBtn" onclick="clearAgentQueue()" title="Очистить очередь" style="display:none">🗑 очередь</button>
|
|
757
860
|
<button class="agent-panel-cancel" id="agentCancelBtn" onclick="cancelAgent()" title="Сбросить состояние агента" style="display:none">⏹ сброс</button>
|
|
861
|
+
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
758
862
|
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
759
863
|
</div>
|
|
864
|
+
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
760
865
|
<div class="agent-terminal" id="agentTerminal"></div>
|
|
761
866
|
</div>
|
|
762
867
|
|
|
@@ -769,205 +874,10 @@ let activeTypes = new Set();
|
|
|
769
874
|
let activePanelKey = null;
|
|
770
875
|
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
771
876
|
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
877
|
+
let e2ePlan = null; // current E2E plan object
|
|
878
|
+
let e2ePlanLoading = false;
|
|
772
879
|
let coverageRunning = false;
|
|
773
880
|
let coverageHasError = false;
|
|
774
|
-
let e2ePlan = null;
|
|
775
|
-
let e2ePlanLoading = false;
|
|
776
|
-
|
|
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
|
-
});
|
|
789
|
-
}
|
|
790
|
-
|
|
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; }
|
|
797
|
-
}
|
|
798
|
-
|
|
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(); }
|
|
806
|
-
}
|
|
807
|
-
|
|
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(); }
|
|
815
|
-
}
|
|
816
|
-
|
|
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
|
-
});
|
|
824
|
-
}
|
|
825
|
-
|
|
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
|
-
});
|
|
832
|
-
}
|
|
833
|
-
|
|
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);
|
|
843
|
-
}
|
|
844
|
-
|
|
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>';
|
|
856
|
-
}
|
|
857
|
-
|
|
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;
|
|
914
|
-
}
|
|
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;
|
|
936
|
-
}
|
|
937
|
-
|
|
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>';
|
|
970
|
-
}
|
|
971
881
|
|
|
972
882
|
// ─── Coverage button ───────────────────────────────────────────────────────────
|
|
973
883
|
function updateCovBtn() {
|
|
@@ -994,6 +904,182 @@ async function runCoverage() {
|
|
|
994
904
|
|
|
995
905
|
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
996
906
|
let agentRunning = false;
|
|
907
|
+
const agentRunningPaths = new Set(); // paths of files currently being processed by agent
|
|
908
|
+
const agentQueuedPaths = new Set(); // paths of files waiting in queue
|
|
909
|
+
|
|
910
|
+
// ─── Console Sessions ─────────────────────────────────────────────────────────
|
|
911
|
+
const consoleSessions = []; // { id, title, lines, status, startTime }
|
|
912
|
+
let activeSessionId = null; // currently viewed tab
|
|
913
|
+
let runningSessionId = null; // tab that is currently receiving output
|
|
914
|
+
const SESSION_MAX = 25;
|
|
915
|
+
const SESSIONS_KEY = 'viberadar_sessions';
|
|
916
|
+
|
|
917
|
+
function _sessionId() {
|
|
918
|
+
return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
function saveSessions() {
|
|
922
|
+
try {
|
|
923
|
+
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
924
|
+
...s, lines: s.lines.slice(-500)
|
|
925
|
+
}));
|
|
926
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(data));
|
|
927
|
+
} catch {}
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
function restoreSessions() {
|
|
931
|
+
try {
|
|
932
|
+
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
933
|
+
if (!raw) return;
|
|
934
|
+
const saved = JSON.parse(raw);
|
|
935
|
+
consoleSessions.push(...saved);
|
|
936
|
+
for (const s of consoleSessions) {
|
|
937
|
+
if (s.status === 'running') {
|
|
938
|
+
s.status = 'error';
|
|
939
|
+
s.lines.push({ text: '⚡ Прервано (перезагрузка страницы)', isError: true });
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
if (consoleSessions.length > 0) {
|
|
943
|
+
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
944
|
+
renderTabs();
|
|
945
|
+
renderActiveSession();
|
|
946
|
+
}
|
|
947
|
+
} catch {}
|
|
948
|
+
}
|
|
949
|
+
|
|
950
|
+
function createSession(title, status = 'running') {
|
|
951
|
+
if (consoleSessions.length >= SESSION_MAX) consoleSessions.shift();
|
|
952
|
+
const s = { id: _sessionId(), title, lines: [], status, startTime: Date.now() };
|
|
953
|
+
consoleSessions.push(s);
|
|
954
|
+
activeSessionId = s.id;
|
|
955
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
956
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
957
|
+
renderTabs();
|
|
958
|
+
renderActiveSession();
|
|
959
|
+
saveSessions();
|
|
960
|
+
return s.id;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function switchSession(id) {
|
|
964
|
+
activeSessionId = id;
|
|
965
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
966
|
+
if (s) {
|
|
967
|
+
const statusText = s.status === 'running' ? 'работает…'
|
|
968
|
+
: s.status === 'ok' ? '✅ готово'
|
|
969
|
+
: s.status === 'error' ? '❌ ошибка'
|
|
970
|
+
: '';
|
|
971
|
+
document.getElementById('agentPanelTitle').textContent = s.title;
|
|
972
|
+
document.getElementById('agentPanelStatus').textContent = statusText;
|
|
973
|
+
}
|
|
974
|
+
renderTabs();
|
|
975
|
+
renderActiveSession();
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
function closeSession(id) {
|
|
979
|
+
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
980
|
+
if (idx === -1) return;
|
|
981
|
+
consoleSessions.splice(idx, 1);
|
|
982
|
+
if (activeSessionId === id) {
|
|
983
|
+
activeSessionId = consoleSessions.length > 0
|
|
984
|
+
? consoleSessions[Math.min(idx, consoleSessions.length - 1)].id
|
|
985
|
+
: null;
|
|
986
|
+
}
|
|
987
|
+
renderTabs();
|
|
988
|
+
renderActiveSession();
|
|
989
|
+
saveSessions();
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
993
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
994
|
+
if (!s) return;
|
|
995
|
+
let stored;
|
|
996
|
+
if (typeof lineOrNode === 'string') {
|
|
997
|
+
stored = { text: lineOrNode, isError, isDim };
|
|
998
|
+
} else {
|
|
999
|
+
stored = { html: lineOrNode.outerHTML };
|
|
1000
|
+
}
|
|
1001
|
+
s.lines.push(stored);
|
|
1002
|
+
if (activeSessionId === id) {
|
|
1003
|
+
const term = document.getElementById('agentTerminal');
|
|
1004
|
+
const el = document.createElement('div');
|
|
1005
|
+
if (stored.html) {
|
|
1006
|
+
el.innerHTML = stored.html;
|
|
1007
|
+
} else {
|
|
1008
|
+
el.className = 'agent-line' + (isError ? ' err' : isDim ? ' dim' : '');
|
|
1009
|
+
el.textContent = lineOrNode;
|
|
1010
|
+
}
|
|
1011
|
+
term.appendChild(el);
|
|
1012
|
+
term.scrollTop = term.scrollHeight;
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
function updateSessionStatus(id, status) {
|
|
1017
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
1018
|
+
if (!s) return;
|
|
1019
|
+
s.status = status;
|
|
1020
|
+
renderTabs();
|
|
1021
|
+
saveSessions();
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
function renderTabs() {
|
|
1025
|
+
const bar = document.getElementById('agentTabsBar');
|
|
1026
|
+
if (!bar) return;
|
|
1027
|
+
bar.innerHTML = consoleSessions.map(s => {
|
|
1028
|
+
const isActive = s.id === activeSessionId;
|
|
1029
|
+
const safe = s.title.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
1030
|
+
return `<div class="agent-tab${isActive ? ' active' : ''}"
|
|
1031
|
+
onclick="switchSession('${s.id}')"
|
|
1032
|
+
onmousedown="if(event.button===1){event.preventDefault();event.stopPropagation();closeSession('${s.id}')}">
|
|
1033
|
+
<span class="tab-dot tab-dot-${s.status}"></span>
|
|
1034
|
+
<span class="agent-tab-title" title="${safe}">${safe}</span>
|
|
1035
|
+
<button class="agent-tab-close" onclick="event.stopPropagation();closeSession('${s.id}')" title="Закрыть вкладку">×</button>
|
|
1036
|
+
</div>`;
|
|
1037
|
+
}).join('');
|
|
1038
|
+
const active = bar.querySelector('.agent-tab.active');
|
|
1039
|
+
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function copyTerminalContent() {
|
|
1043
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1044
|
+
if (!s || s.lines.length === 0) return;
|
|
1045
|
+
// Extract plain text from each line (strip HTML for rich nodes)
|
|
1046
|
+
const text = s.lines.map(l => {
|
|
1047
|
+
if (l.text !== undefined) return l.text;
|
|
1048
|
+
if (l.html) {
|
|
1049
|
+
const tmp = document.createElement('div');
|
|
1050
|
+
tmp.innerHTML = l.html;
|
|
1051
|
+
return tmp.innerText;
|
|
1052
|
+
}
|
|
1053
|
+
return '';
|
|
1054
|
+
}).filter(Boolean).join('\n');
|
|
1055
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
1056
|
+
const btn = document.getElementById('agentCopyBtn');
|
|
1057
|
+
if (!btn) return;
|
|
1058
|
+
const prev = btn.textContent;
|
|
1059
|
+
btn.textContent = '✓';
|
|
1060
|
+
btn.classList.add('copied');
|
|
1061
|
+
setTimeout(() => { btn.textContent = prev; btn.classList.remove('copied'); }, 1500);
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
|
|
1065
|
+
function renderActiveSession() {
|
|
1066
|
+
const term = document.getElementById('agentTerminal');
|
|
1067
|
+
if (!term) return;
|
|
1068
|
+
term.innerHTML = '';
|
|
1069
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1070
|
+
if (!s) return;
|
|
1071
|
+
for (const ln of s.lines) {
|
|
1072
|
+
const el = document.createElement('div');
|
|
1073
|
+
if (ln.html) {
|
|
1074
|
+
el.innerHTML = ln.html;
|
|
1075
|
+
} else {
|
|
1076
|
+
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1077
|
+
el.textContent = ln.text;
|
|
1078
|
+
}
|
|
1079
|
+
term.appendChild(el);
|
|
1080
|
+
}
|
|
1081
|
+
term.scrollTop = term.scrollHeight;
|
|
1082
|
+
}
|
|
997
1083
|
|
|
998
1084
|
async function setAgent(agent) {
|
|
999
1085
|
await fetch('/api/set-agent', {
|
|
@@ -1004,30 +1090,182 @@ async function setAgent(agent) {
|
|
|
1004
1090
|
// scheduleRescan will fire → data-updated → D.agent updates
|
|
1005
1091
|
}
|
|
1006
1092
|
|
|
1093
|
+
async function setModel(model) {
|
|
1094
|
+
await fetch('/api/set-model', {
|
|
1095
|
+
method: 'POST',
|
|
1096
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1097
|
+
body: JSON.stringify({ model }),
|
|
1098
|
+
});
|
|
1099
|
+
// scheduleRescan will fire → data-updated → D.model updates
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1007
1102
|
function setAgentRunning(val) {
|
|
1008
1103
|
agentRunning = val;
|
|
1009
1104
|
document.getElementById('agentCancelBtn').style.display = val ? 'inline-block' : 'none';
|
|
1010
1105
|
}
|
|
1011
1106
|
|
|
1107
|
+
// Returns array of normalized relative paths affected by a given task
|
|
1108
|
+
function getTaskFilePaths(task, featureKey, filePath) {
|
|
1109
|
+
if ((task === 'write-tests-file' || task === 'fix-tests') && filePath) {
|
|
1110
|
+
return [filePath.replace(/\\/g, '/')];
|
|
1111
|
+
}
|
|
1112
|
+
if ((task === 'write-tests' || task === 'fix-tests-all') && featureKey && D?.modules) {
|
|
1113
|
+
return D.modules
|
|
1114
|
+
.filter(m => m.featureKeys?.includes(featureKey) && m.type !== 'test' && (!m.hasTests || m.testStale))
|
|
1115
|
+
.map(m => m.relativePath.replace(/\\/g, '/'));
|
|
1116
|
+
}
|
|
1117
|
+
return [];
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Returns 'running', 'queued', or null for a given relative path
|
|
1121
|
+
function isFileAgentActive(relPath) {
|
|
1122
|
+
const n = relPath.replace(/\\/g, '/');
|
|
1123
|
+
if (agentRunningPaths.has(n)) return 'running';
|
|
1124
|
+
if (agentQueuedPaths.has(n)) return 'queued';
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
function updateQueueBadge(n) {
|
|
1129
|
+
document.getElementById('agentQueueCount').textContent = n;
|
|
1130
|
+
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1131
|
+
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1012
1134
|
async function cancelAgent() {
|
|
1013
1135
|
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1014
1136
|
setAgentRunning(false);
|
|
1137
|
+
updateQueueBadge(0);
|
|
1015
1138
|
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1016
|
-
|
|
1139
|
+
if (runningSessionId) {
|
|
1140
|
+
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1141
|
+
updateSessionStatus(runningSessionId, 'error');
|
|
1142
|
+
runningSessionId = null;
|
|
1143
|
+
} else {
|
|
1144
|
+
appendTerminalLine('⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1145
|
+
}
|
|
1017
1146
|
}
|
|
1018
1147
|
|
|
1019
|
-
async function
|
|
1020
|
-
|
|
1021
|
-
|
|
1148
|
+
async function clearAgentQueue() {
|
|
1149
|
+
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1150
|
+
updateQueueBadge(0);
|
|
1151
|
+
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
// ─── File row more menu ──────────────────────────────────────────────────────
|
|
1155
|
+
let _openFileMenu = null;
|
|
1156
|
+
|
|
1157
|
+
function toggleFileMenu(btn, featureKey, relPath) {
|
|
1158
|
+
const dropdown = btn.nextElementSibling;
|
|
1159
|
+
const isOpen = dropdown.classList.contains('open');
|
|
1160
|
+
// Close any open menu first
|
|
1161
|
+
if (_openFileMenu && _openFileMenu !== dropdown) {
|
|
1162
|
+
_openFileMenu.classList.remove('open');
|
|
1163
|
+
}
|
|
1164
|
+
dropdown.classList.toggle('open', !isOpen);
|
|
1165
|
+
_openFileMenu = isOpen ? null : dropdown;
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Close menu on outside click
|
|
1169
|
+
document.addEventListener('click', () => {
|
|
1170
|
+
if (_openFileMenu) { _openFileMenu.classList.remove('open'); _openFileMenu = null; }
|
|
1171
|
+
});
|
|
1172
|
+
|
|
1173
|
+
async function copyPromptForFile(featureKey, relPath, dropdown) {
|
|
1174
|
+
if (dropdown) { dropdown.classList.remove('open'); _openFileMenu = null; }
|
|
1175
|
+
try {
|
|
1176
|
+
const res = await fetch('/api/get-prompt', {
|
|
1177
|
+
method: 'POST',
|
|
1178
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1179
|
+
body: JSON.stringify({ task: 'write-tests-file', featureKey, filePath: relPath }),
|
|
1180
|
+
});
|
|
1181
|
+
const { prompt, error } = await res.json();
|
|
1182
|
+
if (error || !prompt) { alert('Не удалось получить промпт: ' + (error || 'пустой')); return; }
|
|
1183
|
+
await navigator.clipboard.writeText(prompt);
|
|
1184
|
+
// Brief visual feedback in terminal
|
|
1185
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
1186
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
1187
|
+
appendTerminalLine(`📋 Промпт для "${relPath.split('/').pop()}" скопирован в буфер обмена`, false);
|
|
1188
|
+
appendTerminalLine(' Вставь его в другой агент (Claude, Codex, ChatGPT) и запусти вручную.', false);
|
|
1189
|
+
} catch (e) {
|
|
1190
|
+
alert('Ошибка копирования: ' + e.message);
|
|
1191
|
+
}
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
// ─── Agent menu ─────────────────────────────────────────────────────────────
|
|
1195
|
+
const CLAUDE_MODELS = [
|
|
1196
|
+
{ id: 'claude-sonnet-4-6', checkId: 'amSonnet46Check' },
|
|
1197
|
+
{ id: 'claude-opus-4-5', checkId: 'amOpus45Check' },
|
|
1198
|
+
{ id: 'claude-haiku-3-5', checkId: 'amHaiku35Check' },
|
|
1199
|
+
];
|
|
1200
|
+
|
|
1201
|
+
function updateAgentBtn() {
|
|
1202
|
+
const btn = document.getElementById('agentBtn');
|
|
1203
|
+
if (!D) return;
|
|
1204
|
+
const a = D.agent;
|
|
1205
|
+
const m = D.model;
|
|
1206
|
+
// Button label: agent + model shortname
|
|
1207
|
+
let label = a === 'claude' ? '🤖 Claude' : a === 'codex' ? '🤖 Codex' : '🤖 не выбран';
|
|
1208
|
+
if (a === 'claude' && m) {
|
|
1209
|
+
// Show short model name: "sonnet-4-6", "opus-4-5", "haiku-3-5"
|
|
1210
|
+
const short = m.replace('claude-', '');
|
|
1211
|
+
label += ` · ${short}`;
|
|
1212
|
+
}
|
|
1213
|
+
btn.textContent = label;
|
|
1214
|
+
// Update agent checkmarks
|
|
1215
|
+
document.getElementById('amClaudeCheck').classList.toggle('active', a === 'claude');
|
|
1216
|
+
document.getElementById('amCodexCheck').classList.toggle('active', a === 'codex');
|
|
1217
|
+
// Show/hide model section (only for claude)
|
|
1218
|
+
const isClaude = a === 'claude';
|
|
1219
|
+
document.getElementById('amModelDivider').style.display = isClaude ? '' : 'none';
|
|
1220
|
+
document.getElementById('amModelLabel').style.display = isClaude ? '' : 'none';
|
|
1221
|
+
CLAUDE_MODELS.forEach(({ id, checkId }) => {
|
|
1222
|
+
const el = document.getElementById(id);
|
|
1223
|
+
if (el) el.style.display = isClaude ? '' : 'none';
|
|
1224
|
+
document.getElementById(checkId).classList.toggle('active', m === id);
|
|
1225
|
+
});
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
function toggleAgentMenu() {
|
|
1229
|
+
const menu = document.getElementById('agentMenu');
|
|
1230
|
+
const isOpen = menu.classList.contains('open');
|
|
1231
|
+
menu.classList.toggle('open');
|
|
1232
|
+
if (!isOpen) {
|
|
1233
|
+
// Close on click outside
|
|
1234
|
+
setTimeout(() => {
|
|
1235
|
+
document.addEventListener('click', closeAgentMenuOnOutside, { once: true });
|
|
1236
|
+
}, 0);
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
function closeAgentMenu() {
|
|
1240
|
+
document.getElementById('agentMenu').classList.remove('open');
|
|
1241
|
+
}
|
|
1242
|
+
function closeAgentMenuOnOutside(e) {
|
|
1243
|
+
if (!e.target.closest('#agentMenu') && !e.target.closest('#agentBtn')) {
|
|
1244
|
+
closeAgentMenu();
|
|
1245
|
+
}
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
async function reauthAgent() {
|
|
1249
|
+
const agent = D?.agent;
|
|
1250
|
+
if (!agent) {
|
|
1251
|
+
appendTerminalLine('⚠ Сначала выбери агента (Claude или Codex)', true);
|
|
1022
1252
|
document.getElementById('agentPanel').classList.add('open');
|
|
1023
1253
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1024
|
-
appendTerminalLine('⚠️ Агент уже запущен. Нажми ⏹ сброс чтобы отменить.', true);
|
|
1025
1254
|
return;
|
|
1026
1255
|
}
|
|
1027
|
-
|
|
1028
|
-
|
|
1256
|
+
const id = createSession('🔑 Перелогинивание');
|
|
1257
|
+
runningSessionId = id;
|
|
1258
|
+
document.getElementById('agentPanelTitle').textContent = '🔑 Перелогинивание';
|
|
1259
|
+
document.getElementById('agentPanelStatus').textContent = 'выполняю…';
|
|
1260
|
+
await fetch('/api/agent-reauth', { method: 'POST' });
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
async function runAgentTask(task, featureKey, filePath) {
|
|
1029
1264
|
document.getElementById('agentPanel').classList.add('open');
|
|
1030
1265
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1266
|
+
if (!agentRunning) {
|
|
1267
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1268
|
+
}
|
|
1031
1269
|
await fetch('/api/run-agent', {
|
|
1032
1270
|
method: 'POST',
|
|
1033
1271
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1036,11 +1274,9 @@ async function runAgentTask(task, featureKey, filePath) {
|
|
|
1036
1274
|
}
|
|
1037
1275
|
|
|
1038
1276
|
async function runTests(featureKey, testType) {
|
|
1039
|
-
document.getElementById('agentTerminal').innerHTML = '';
|
|
1040
|
-
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
1041
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1042
1277
|
document.getElementById('agentPanel').classList.add('open');
|
|
1043
1278
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1279
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1044
1280
|
await fetch('/api/run-tests', {
|
|
1045
1281
|
method: 'POST',
|
|
1046
1282
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -1061,12 +1297,11 @@ function toggleAgentPanel() {
|
|
|
1061
1297
|
}
|
|
1062
1298
|
|
|
1063
1299
|
function appendTerminalLine(line, isError, isDim) {
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
term.scrollTop = term.scrollHeight;
|
|
1300
|
+
if (!activeSessionId) {
|
|
1301
|
+
const id = createSession('Консоль', 'info');
|
|
1302
|
+
activeSessionId = id;
|
|
1303
|
+
}
|
|
1304
|
+
appendToSession(activeSessionId, line, !!isError, !!isDim);
|
|
1070
1305
|
}
|
|
1071
1306
|
|
|
1072
1307
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
@@ -1113,6 +1348,7 @@ async function init() {
|
|
|
1113
1348
|
coverageHasError = status.coverageError ?? false;
|
|
1114
1349
|
}
|
|
1115
1350
|
updateCovBtn();
|
|
1351
|
+
updateAgentBtn();
|
|
1116
1352
|
|
|
1117
1353
|
document.getElementById('projectName').textContent = D.projectName;
|
|
1118
1354
|
document.getElementById('scannedAt').textContent =
|
|
@@ -1364,32 +1600,179 @@ function renderFeatureCards(c) {
|
|
|
1364
1600
|
}
|
|
1365
1601
|
}
|
|
1366
1602
|
|
|
1603
|
+
// ─── E2E Plan functions ────────────────────────────────────────────────────────
|
|
1604
|
+
|
|
1605
|
+
async function generateE2ePlan(featureKey) {
|
|
1606
|
+
if (D.agentRunning) { alert('Агент уже запущен'); return; }
|
|
1607
|
+
await fetch('/api/e2e/generate-plan', {
|
|
1608
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1609
|
+
body: JSON.stringify({ featureKey })
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
|
|
1613
|
+
async function loadE2ePlan(featureKey) {
|
|
1614
|
+
e2ePlanLoading = true;
|
|
1615
|
+
e2ePlan = null;
|
|
1616
|
+
try {
|
|
1617
|
+
const r = await fetch(`/api/e2e/plan/${encodeURIComponent(featureKey)}`);
|
|
1618
|
+
if (r.ok) e2ePlan = await r.json();
|
|
1619
|
+
} catch {}
|
|
1620
|
+
e2ePlanLoading = false;
|
|
1621
|
+
}
|
|
1622
|
+
|
|
1623
|
+
async function reviewE2eCase(featureKey, testCaseId, status) {
|
|
1624
|
+
const r = await fetch('/api/e2e/review', {
|
|
1625
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1626
|
+
body: JSON.stringify({ featureKey, testCaseId, status })
|
|
1627
|
+
});
|
|
1628
|
+
if (r.ok) { const data = await r.json(); e2ePlan = data.plan; renderContent(); }
|
|
1629
|
+
}
|
|
1630
|
+
|
|
1631
|
+
async function reviewAllE2e(featureKey, status) {
|
|
1632
|
+
const r = await fetch('/api/e2e/review-all', {
|
|
1633
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1634
|
+
body: JSON.stringify({ featureKey, status })
|
|
1635
|
+
});
|
|
1636
|
+
if (r.ok) { const data = await r.json(); e2ePlan = data.plan; renderContent(); }
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
async function writeE2eTests(featureKey) {
|
|
1640
|
+
if (D.agentRunning) { alert('Агент уже запущен'); return; }
|
|
1641
|
+
await fetch('/api/e2e/write-tests', {
|
|
1642
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1643
|
+
body: JSON.stringify({ featureKey })
|
|
1644
|
+
});
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
async function runE2eTests(featureKey) {
|
|
1648
|
+
await fetch('/api/e2e/run-tests', {
|
|
1649
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
1650
|
+
body: JSON.stringify({ featureKey })
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
function openScreenshot(src) {
|
|
1655
|
+
const modal = document.createElement('div');
|
|
1656
|
+
modal.className = 'e2e-screenshot-modal';
|
|
1657
|
+
modal.innerHTML = `<img src="${src}" />`;
|
|
1658
|
+
modal.onclick = () => modal.remove();
|
|
1659
|
+
document.body.appendChild(modal);
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1662
|
+
function statusBadge(status) {
|
|
1663
|
+
const labels = { pending:'⏳ ожидает', approved:'✅ одобрен', rejected:'❌ отклонён', written:'✍ написан', passed:'✅ прошёл', failed:'❌ упал' };
|
|
1664
|
+
return `<span class="e2e-status-badge ${status}">${labels[status] || status}</span>`;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
function renderE2eTestCase(tc, featureKey) {
|
|
1668
|
+
const screenshots = (tc.screenshotPaths || []).map(p =>
|
|
1669
|
+
`<img class="e2e-screenshot" src="/api/e2e/screenshot/${encodeURIComponent(p)}" onclick="openScreenshot(this.src)" />`
|
|
1670
|
+
).join('');
|
|
1671
|
+
const screenshotStrip = screenshots ? `<div class="e2e-screenshot-strip">${screenshots}</div>` : '';
|
|
1672
|
+
const errorBlock = tc.lastError ? `<div class="e2e-error">❌ ${tc.lastError}</div>` : '';
|
|
1673
|
+
return `
|
|
1674
|
+
<div class="e2e-case ${tc.status}" id="e2e-case-${tc.id}">
|
|
1675
|
+
<div class="e2e-case-header">
|
|
1676
|
+
<div class="e2e-case-name">${tc.name}</div>
|
|
1677
|
+
${statusBadge(tc.status)}
|
|
1678
|
+
</div>
|
|
1679
|
+
<div class="e2e-case-desc">${tc.description}</div>
|
|
1680
|
+
<div class="e2e-steps"><ol>${tc.steps.map(s => `<li>${s}</li>`).join('')}</ol></div>
|
|
1681
|
+
<div class="e2e-expected">Ожидается: ${tc.expectedResults.join('; ')}</div>
|
|
1682
|
+
${errorBlock}
|
|
1683
|
+
${screenshotStrip}
|
|
1684
|
+
<div class="e2e-case-actions">
|
|
1685
|
+
<button class="e2e-btn e2e-btn-approve" onclick="reviewE2eCase('${featureKey}','${tc.id}','approved')">✅ Одобрить</button>
|
|
1686
|
+
<button class="e2e-btn e2e-btn-reject" onclick="reviewE2eCase('${featureKey}','${tc.id}','rejected')">❌ Отклонить</button>
|
|
1687
|
+
</div>
|
|
1688
|
+
</div>`;
|
|
1689
|
+
}
|
|
1690
|
+
|
|
1691
|
+
function renderE2ePlanView(c) {
|
|
1692
|
+
const featureKey = drillFeatureKey;
|
|
1693
|
+
const feat = D.features.find(f => f.key === featureKey);
|
|
1694
|
+
|
|
1695
|
+
if (e2ePlanLoading) {
|
|
1696
|
+
c.innerHTML = `<div style="padding:40px;text-align:center;color:var(--dim)">Загружаю план…</div>`;
|
|
1697
|
+
return;
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
if (!e2ePlan) {
|
|
1701
|
+
const hasPW = D.hasPlaywright;
|
|
1702
|
+
c.innerHTML = `
|
|
1703
|
+
<div class="drill-header">
|
|
1704
|
+
<button class="back-btn" onclick="drillTestType=null;renderContent()">← Назад</button>
|
|
1705
|
+
<div class="drill-title"><span>🎭 E2E план — ${feat?.label || featureKey}</span></div>
|
|
1706
|
+
</div>
|
|
1707
|
+
<div style="padding:40px;text-align:center;border:1px dashed var(--border);border-radius:8px;margin-top:16px">
|
|
1708
|
+
${!hasPW ? '<div style="color:#f85149;margin-bottom:12px">⚠️ @playwright/test не найден в проекте</div>' : ''}
|
|
1709
|
+
<div style="color:var(--dim);margin-bottom:16px">E2E план не создан. AI проанализирует файлы фичи и предложит тест-кейсы.</div>
|
|
1710
|
+
<button class="e2e-btn e2e-btn-primary" onclick="generateE2ePlan('${featureKey}')">▶ Сгенерировать план</button>
|
|
1711
|
+
</div>`;
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
const total = e2ePlan.testCases.length;
|
|
1716
|
+
const approved = e2ePlan.testCases.filter(t => t.status === 'approved').length;
|
|
1717
|
+
const rejected = e2ePlan.testCases.filter(t => t.status === 'rejected').length;
|
|
1718
|
+
const written = e2ePlan.testCases.filter(t => ['written','passed','failed'].includes(t.status)).length;
|
|
1719
|
+
const passed = e2ePlan.testCases.filter(t => t.status === 'passed').length;
|
|
1720
|
+
const failed = e2ePlan.testCases.filter(t => t.status === 'failed').length;
|
|
1721
|
+
|
|
1722
|
+
c.innerHTML = `
|
|
1723
|
+
<div class="drill-header">
|
|
1724
|
+
<button class="back-btn" onclick="drillTestType=null;renderContent()">← Назад</button>
|
|
1725
|
+
<div class="drill-title"><span>🎭 E2E план — ${feat?.label || featureKey}</span></div>
|
|
1726
|
+
</div>
|
|
1727
|
+
<div class="e2e-plan-container">
|
|
1728
|
+
<div class="e2e-batch-bar">
|
|
1729
|
+
<button class="e2e-btn e2e-btn-approve" onclick="reviewAllE2e('${featureKey}','approved')">✅ Одобрить все</button>
|
|
1730
|
+
<button class="e2e-btn e2e-btn-reject" onclick="reviewAllE2e('${featureKey}','rejected')">❌ Отклонить все</button>
|
|
1731
|
+
<button class="e2e-btn e2e-btn-primary" onclick="writeE2eTests('${featureKey}')" ${approved===0?'disabled':''}>✍ Написать тесты (${approved})</button>
|
|
1732
|
+
<button class="e2e-btn e2e-btn-secondary" onclick="runE2eTests('${featureKey}')" ${written===0?'disabled':''}>▶ Запустить E2E</button>
|
|
1733
|
+
<button class="e2e-btn e2e-btn-secondary" onclick="generateE2ePlan('${featureKey}')">🔄 Перегенерировать</button>
|
|
1734
|
+
<span class="e2e-stats">${total} кейсов · ✅ ${passed} · ❌ ${failed} · одобрено: ${approved} · отклонено: ${rejected}</span>
|
|
1735
|
+
</div>
|
|
1736
|
+
${e2ePlan.testCases.map(tc => renderE2eTestCase(tc, featureKey)).join('')}
|
|
1737
|
+
</div>`;
|
|
1738
|
+
}
|
|
1739
|
+
|
|
1367
1740
|
function testTypeCard(type, label, icon, color, count, active, featureKey, failedCount = 0) {
|
|
1368
|
-
const empty
|
|
1741
|
+
const empty = count === 0 && type !== 'source' && type !== 'e2e';
|
|
1369
1742
|
const hasFailed = failedCount > 0 && type !== 'source';
|
|
1370
|
-
const
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
const
|
|
1375
|
-
|
|
1743
|
+
const subLabel = type === 'source' ? 'файлов'
|
|
1744
|
+
: type === 'e2e' ? (count > 0 ? pluralFiles(count) : (D.e2ePlansExist?.[featureKey] ? '📋 план создан' : 'нет тестов'))
|
|
1745
|
+
: (empty ? 'нет тестов' : pluralFiles(count));
|
|
1746
|
+
const accentColor = hasFailed ? 'var(--red)' : color;
|
|
1747
|
+
const countColor = hasFailed ? 'var(--red)' : (active || !empty || type === 'e2e' ? color : 'var(--dim)');
|
|
1748
|
+
|
|
1749
|
+
const failedBadge = hasFailed
|
|
1750
|
+
? `<span class="tt-failed-badge">❌ ${failedCount}</span>`
|
|
1376
1751
|
: '';
|
|
1377
|
-
|
|
1378
|
-
const
|
|
1379
|
-
|
|
1380
|
-
? `<div style="font-size:11px;color:var(--red);margin-top:2px;font-weight:600">❌ ${failedCount} упало</div>`
|
|
1752
|
+
|
|
1753
|
+
const runBtn = !empty && type !== 'source' && type !== 'e2e' && featureKey
|
|
1754
|
+
? `<button class="tt-run-btn" onclick="event.stopPropagation();runTests('${featureKey}','${type}')">▶ запустить тесты</button>`
|
|
1381
1755
|
: '';
|
|
1382
|
-
|
|
1383
|
-
|
|
1756
|
+
const fixAllBtn = hasFailed && D.agent && featureKey
|
|
1757
|
+
? `<button class="tt-run-btn tt-fix-btn" onclick="event.stopPropagation();runAgentTask('fix-tests-all','${featureKey}','${type}')">🔧 починить все</button>`
|
|
1758
|
+
: '';
|
|
1759
|
+
const writeBtn = type !== 'source' && type !== 'e2e' && D.agent && featureKey
|
|
1760
|
+
? `<button class="tt-run-btn tt-write-btn" onclick="event.stopPropagation();runAgentTask('write-tests','${featureKey}')">✍ написать тесты</button>`
|
|
1761
|
+
: '';
|
|
1762
|
+
const actions = (runBtn || fixAllBtn || writeBtn)
|
|
1763
|
+
? `<div class="tt-card-actions">${runBtn}${fixAllBtn}${writeBtn}</div>`
|
|
1764
|
+
: '';
|
|
1765
|
+
|
|
1384
1766
|
return `
|
|
1385
|
-
<div class="test-type-card${empty
|
|
1386
|
-
data-testtype="${type}" style="
|
|
1387
|
-
<div class="tt-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1767
|
+
<div class="test-type-card${empty ? ' tt-empty' : ''}${active ? ' tt-active' : ''}${hasFailed ? ' tt-failed' : ''}"
|
|
1768
|
+
data-testtype="${type}" style="border-top-color:${accentColor}">
|
|
1769
|
+
<div class="tt-card-header">
|
|
1770
|
+
<span class="tt-label">${icon} ${label}</span>
|
|
1771
|
+
${failedBadge}
|
|
1772
|
+
</div>
|
|
1773
|
+
<span class="tt-count" style="color:${countColor}">${count}</span>
|
|
1774
|
+
<span class="tt-sub">${subLabel}</span>
|
|
1775
|
+
${actions}
|
|
1393
1776
|
</div>`;
|
|
1394
1777
|
}
|
|
1395
1778
|
|
|
@@ -1397,9 +1780,9 @@ function renderFeatureDetail(c) {
|
|
|
1397
1780
|
const feat = D.features.find(f => f.key === drillFeatureKey);
|
|
1398
1781
|
if (!feat) { backToFeatures(); return; }
|
|
1399
1782
|
|
|
1400
|
-
// E2E plan view
|
|
1783
|
+
// E2E tab → show plan view instead of file list
|
|
1401
1784
|
if (drillTestType === 'e2e') {
|
|
1402
|
-
renderE2ePlanView(c
|
|
1785
|
+
renderE2ePlanView(c);
|
|
1403
1786
|
return;
|
|
1404
1787
|
}
|
|
1405
1788
|
|
|
@@ -1632,12 +2015,16 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1632
2015
|
const dir = parts.slice(0, -1).join('/');
|
|
1633
2016
|
|
|
1634
2017
|
// Test errors from last run
|
|
1635
|
-
const testErr
|
|
1636
|
-
const
|
|
2018
|
+
const testErr = isTest && D.testErrors && D.testErrors[relPath];
|
|
2019
|
+
const agentState = !isTest ? isFileAgentActive(relPath) : null;
|
|
2020
|
+
const icon = agentState
|
|
2021
|
+
? `<span class="file-agent-spinner ${agentState}" title="${agentState === 'running' ? 'Агент работает…' : 'В очереди…'}"></span>`
|
|
2022
|
+
: (testErr ? '❌' : isTest ? '🧪' : (m.testStale ? '⚠️' : m.hasTests ? '✅' : '⬜'));
|
|
1637
2023
|
const isActive = activePanelKey === m.id;
|
|
1638
2024
|
|
|
1639
2025
|
// Write-test button for source files
|
|
1640
|
-
|
|
2026
|
+
// In feature drill-down (featureKey set), show button even for isInfra files — user explicitly added them to the feature
|
|
2027
|
+
const showAgentBtn = D.agent && !isTest && featureKey && (!m.hasTests || m.testStale);
|
|
1641
2028
|
const agentBtnLabel = m.testStale ? '↻ обновить тест' : '✍ написать тест';
|
|
1642
2029
|
const agentBtnTitle = m.testStale ? 'Файл изменился — обновить тесты' : 'Написать тест для этого файла';
|
|
1643
2030
|
const agentBtn = showAgentBtn
|
|
@@ -1668,6 +2055,20 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1668
2055
|
</div>`
|
|
1669
2056
|
: '';
|
|
1670
2057
|
|
|
2058
|
+
// "⋯" more menu — always show for source files in feature context
|
|
2059
|
+
const moreBtn = !isTest && featureKey
|
|
2060
|
+
? `<div class="file-row-more-wrap">
|
|
2061
|
+
<button class="file-row-more-btn" title="Ещё"
|
|
2062
|
+
onclick="event.stopPropagation();toggleFileMenu(this,'${featureKey}','${relPath}')">⋯</button>
|
|
2063
|
+
<div class="file-row-more-dropdown">
|
|
2064
|
+
<button class="file-row-more-item"
|
|
2065
|
+
onclick="event.stopPropagation();copyPromptForFile('${featureKey}','${relPath}',this.closest('.file-row-more-dropdown'))">
|
|
2066
|
+
📋 Скопировать промпт для агента
|
|
2067
|
+
</button>
|
|
2068
|
+
</div>
|
|
2069
|
+
</div>`
|
|
2070
|
+
: '';
|
|
2071
|
+
|
|
1671
2072
|
return `
|
|
1672
2073
|
<div class="file-row${isActive ? ' active' : ''}${testErr ? ' has-errors' : ''}" data-id="${m.id}">
|
|
1673
2074
|
<span class="file-row-icon">${icon}</span>
|
|
@@ -1675,6 +2076,7 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
1675
2076
|
${errBadge}
|
|
1676
2077
|
${agentBtn}
|
|
1677
2078
|
${fixBtn}
|
|
2079
|
+
${moreBtn}
|
|
1678
2080
|
<span class="file-row-dir">${dir}</span>
|
|
1679
2081
|
</div>
|
|
1680
2082
|
${errHtml}`;
|
|
@@ -1770,7 +2172,11 @@ function fileItem(m, isTest = false) {
|
|
|
1770
2172
|
const parts = m.relativePath.replace(/\\/g, '/').split('/');
|
|
1771
2173
|
const name = parts[parts.length - 1];
|
|
1772
2174
|
const dir = parts.slice(0, -1).join('/');
|
|
1773
|
-
const
|
|
2175
|
+
const relPathNorm = m.relativePath.replace(/\\/g, '/');
|
|
2176
|
+
const agentStateItem = !isTest ? isFileAgentActive(relPathNorm) : null;
|
|
2177
|
+
const icon = agentStateItem
|
|
2178
|
+
? `<span class="file-agent-spinner ${agentStateItem}" title="${agentStateItem === 'running' ? 'Агент работает…' : 'В очереди…'}"></span>`
|
|
2179
|
+
: (isTest ? '🧪' : (m.hasTests ? '✅' : '⬜'));
|
|
1774
2180
|
return `
|
|
1775
2181
|
<div class="file-item">
|
|
1776
2182
|
<span class="file-item-icon">${icon}</span>
|
|
@@ -1968,6 +2374,7 @@ async function refreshData() {
|
|
|
1968
2374
|
renderStats();
|
|
1969
2375
|
renderSidebar();
|
|
1970
2376
|
renderContent();
|
|
2377
|
+
updateAgentBtn();
|
|
1971
2378
|
|
|
1972
2379
|
// Re-render drill-down or re-open panel
|
|
1973
2380
|
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
@@ -2021,31 +2428,69 @@ function connectSSE() {
|
|
|
2021
2428
|
setTimeout(() => { coverageHasError = false; updateCovBtn(); }, 8000);
|
|
2022
2429
|
});
|
|
2023
2430
|
|
|
2431
|
+
es.addEventListener('agent-queued', (e) => {
|
|
2432
|
+
const { queueLength, title, task, featureKey, filePath } = JSON.parse(e.data);
|
|
2433
|
+
updateQueueBadge(queueLength);
|
|
2434
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
2435
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
2436
|
+
// Track queued paths for spinner
|
|
2437
|
+
getTaskFilePaths(task, featureKey, filePath).forEach(p => agentQueuedPaths.add(p));
|
|
2438
|
+
renderContent();
|
|
2439
|
+
// Append queue notification to the currently running session (or active)
|
|
2440
|
+
const targetId = runningSessionId || activeSessionId;
|
|
2441
|
+
if (targetId) {
|
|
2442
|
+
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
2443
|
+
}
|
|
2444
|
+
});
|
|
2445
|
+
|
|
2024
2446
|
es.addEventListener('agent-started', (e) => {
|
|
2025
2447
|
setAgentRunning(true);
|
|
2026
|
-
const { title } = JSON.parse(e.data);
|
|
2448
|
+
const { title, queueLength = 0, task, featureKey, filePath } = JSON.parse(e.data);
|
|
2449
|
+
updateQueueBadge(queueLength);
|
|
2450
|
+
// Move paths from queued → running (current task)
|
|
2451
|
+
const startedPaths = getTaskFilePaths(task, featureKey, filePath);
|
|
2452
|
+
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
2453
|
+
renderContent();
|
|
2454
|
+
// Close previous session (queue case: agent-done not fired between tasks)
|
|
2455
|
+
if (runningSessionId) {
|
|
2456
|
+
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
2457
|
+
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
2458
|
+
}
|
|
2459
|
+
const id = createSession(title);
|
|
2460
|
+
runningSessionId = id;
|
|
2027
2461
|
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
2028
2462
|
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
2029
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
2030
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
2031
|
-
document.getElementById('agentTerminal').innerHTML = '';
|
|
2032
2463
|
});
|
|
2033
2464
|
|
|
2034
2465
|
es.addEventListener('agent-output', (e) => {
|
|
2035
2466
|
const { line, isError, isDim } = JSON.parse(e.data);
|
|
2036
|
-
|
|
2037
|
-
|
|
2467
|
+
if (runningSessionId) {
|
|
2468
|
+
appendToSession(runningSessionId, line, !!isError, !!isDim);
|
|
2469
|
+
if (activeSessionId === runningSessionId) {
|
|
2470
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
2471
|
+
}
|
|
2472
|
+
} else {
|
|
2473
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
2474
|
+
}
|
|
2038
2475
|
});
|
|
2039
2476
|
|
|
2040
2477
|
es.addEventListener('agent-done', () => {
|
|
2041
2478
|
setAgentRunning(false);
|
|
2042
|
-
|
|
2479
|
+
updateQueueBadge(0);
|
|
2480
|
+
agentRunningPaths.clear();
|
|
2481
|
+
agentQueuedPaths.clear();
|
|
2482
|
+
if (runningSessionId) {
|
|
2483
|
+
updateSessionStatus(runningSessionId, 'ok');
|
|
2484
|
+
if (activeSessionId === runningSessionId) {
|
|
2485
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
2486
|
+
}
|
|
2487
|
+
runningSessionId = null;
|
|
2488
|
+
}
|
|
2043
2489
|
renderContent();
|
|
2044
2490
|
});
|
|
2045
2491
|
|
|
2046
2492
|
es.addEventListener('agent-summary', (e) => {
|
|
2047
2493
|
const { passed, failed, files } = JSON.parse(e.data);
|
|
2048
|
-
const term = document.getElementById('agentTerminal');
|
|
2049
2494
|
const allOk = failed === 0;
|
|
2050
2495
|
const box = document.createElement('div');
|
|
2051
2496
|
box.style.cssText = `
|
|
@@ -2062,64 +2507,114 @@ function connectSSE() {
|
|
|
2062
2507
|
</div>
|
|
2063
2508
|
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
2064
2509
|
`;
|
|
2065
|
-
|
|
2066
|
-
|
|
2510
|
+
const targetId = runningSessionId || activeSessionId;
|
|
2511
|
+
if (targetId) {
|
|
2512
|
+
appendToSession(targetId, box);
|
|
2513
|
+
// Mark session status from test results immediately
|
|
2514
|
+
if (runningSessionId === targetId) {
|
|
2515
|
+
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2067
2518
|
});
|
|
2068
2519
|
|
|
2069
2520
|
es.addEventListener('agent-error', (e) => {
|
|
2070
2521
|
setAgentRunning(false);
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2522
|
+
agentRunningPaths.clear();
|
|
2523
|
+
agentQueuedPaths.clear();
|
|
2524
|
+
renderContent();
|
|
2525
|
+
const { message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
2526
|
+
|
|
2527
|
+
// Build error node (plain text or auth/install box)
|
|
2528
|
+
let node = null;
|
|
2529
|
+
if (authRequired || notInstalled) {
|
|
2530
|
+
node = document.createElement('div');
|
|
2531
|
+
node.style.cssText = 'margin:10px 0 4px;padding:10px 14px;border-radius:8px;border:1px solid var(--red);background:#2a0d0d;font-family:inherit;';
|
|
2532
|
+
node.innerHTML = `
|
|
2533
|
+
<div style="font-size:13px;font-weight:700;color:var(--red)">❌ ${message || 'Ошибка агента'}</div>
|
|
2534
|
+
${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>` : ''}
|
|
2535
|
+
${notInstalled ? `<div style="margin-top:6px;font-size:11px;color:var(--dim)">После установки перезапусти viberadar</div>` : ''}
|
|
2536
|
+
`;
|
|
2537
|
+
}
|
|
2538
|
+
|
|
2539
|
+
// If no session exists yet (startup check fires before any run), create one
|
|
2540
|
+
const targetId = runningSessionId || (() => {
|
|
2541
|
+
if (authRequired || notInstalled) {
|
|
2542
|
+
const id = createSession('⚠️ Проверка агента', 'error');
|
|
2543
|
+
return id;
|
|
2544
|
+
}
|
|
2545
|
+
return activeSessionId;
|
|
2546
|
+
})();
|
|
2547
|
+
|
|
2548
|
+
if (targetId) {
|
|
2549
|
+
if (node) {
|
|
2550
|
+
appendToSession(targetId, node);
|
|
2551
|
+
} else {
|
|
2552
|
+
appendToSession(targetId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
2553
|
+
}
|
|
2554
|
+
updateSessionStatus(targetId, 'error');
|
|
2555
|
+
if (activeSessionId === targetId) {
|
|
2556
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
if (runningSessionId) runningSessionId = null;
|
|
2074
2560
|
});
|
|
2075
2561
|
|
|
2076
2562
|
es.addEventListener('tests-started', (e) => {
|
|
2077
2563
|
const { testType, count } = JSON.parse(e.data);
|
|
2564
|
+
const id = createSession(`🧪 ${testType}`);
|
|
2565
|
+
runningSessionId = id;
|
|
2566
|
+
appendToSession(id, `Запускаю ${testType} тесты (${count} файлов)…`);
|
|
2078
2567
|
document.getElementById('agentPanelTitle').textContent = `🧪 ${testType} тесты`;
|
|
2079
2568
|
document.getElementById('agentPanelStatus').textContent = `запускаю ${count} файлов…`;
|
|
2080
2569
|
});
|
|
2081
2570
|
|
|
2082
2571
|
es.addEventListener('tests-done', (e) => {
|
|
2083
2572
|
const { passed, failed, testErrors } = JSON.parse(e.data);
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2573
|
+
const status = failed === 0 ? 'ok' : 'error';
|
|
2574
|
+
if (runningSessionId) {
|
|
2575
|
+
updateSessionStatus(runningSessionId, status);
|
|
2576
|
+
if (activeSessionId === runningSessionId) {
|
|
2577
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
2578
|
+
failed === 0 ? `✅ ${passed} passed` : `⚠️ ${passed} passed, ${failed} failed`;
|
|
2579
|
+
}
|
|
2580
|
+
runningSessionId = null;
|
|
2581
|
+
}
|
|
2087
2582
|
D.testErrors = testErrors || {};
|
|
2088
2583
|
renderContent();
|
|
2089
2584
|
});
|
|
2090
2585
|
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
|
|
2094
|
-
|
|
2586
|
+
es.addEventListener('e2e-plan-generating', (e) => {
|
|
2587
|
+
const { featureKey } = JSON.parse(e.data);
|
|
2588
|
+
if (drillFeatureKey === featureKey && drillTestType === 'e2e') {
|
|
2589
|
+
e2ePlanLoading = true;
|
|
2590
|
+
renderContent();
|
|
2591
|
+
}
|
|
2095
2592
|
});
|
|
2096
2593
|
|
|
2097
2594
|
es.addEventListener('e2e-plan-ready', (e) => {
|
|
2098
|
-
const
|
|
2099
|
-
|
|
2100
|
-
|
|
2101
|
-
|
|
2102
|
-
renderContent();
|
|
2595
|
+
const { featureKey, plan } = JSON.parse(e.data);
|
|
2596
|
+
if (drillFeatureKey === featureKey) {
|
|
2597
|
+
e2ePlan = plan;
|
|
2598
|
+
e2ePlanLoading = false;
|
|
2599
|
+
if (drillTestType === 'e2e') renderContent();
|
|
2103
2600
|
}
|
|
2104
2601
|
});
|
|
2105
2602
|
|
|
2106
2603
|
es.addEventListener('e2e-plan-error', (e) => {
|
|
2604
|
+
const { featureKey, message } = JSON.parse(e.data);
|
|
2107
2605
|
e2ePlanLoading = false;
|
|
2108
|
-
|
|
2109
|
-
if (drillFeatureKey === data.featureKey) {
|
|
2110
|
-
e2ePlan = null;
|
|
2606
|
+
if (drillFeatureKey === featureKey && drillTestType === 'e2e') {
|
|
2111
2607
|
renderContent();
|
|
2608
|
+
setTimeout(() => alert('Ошибка генерации плана: ' + message), 100);
|
|
2112
2609
|
}
|
|
2113
2610
|
});
|
|
2114
2611
|
|
|
2115
2612
|
es.addEventListener('e2e-tests-done', (e) => {
|
|
2116
|
-
const
|
|
2117
|
-
if (drillFeatureKey ===
|
|
2118
|
-
e2ePlan =
|
|
2119
|
-
renderContent();
|
|
2613
|
+
const { featureKey, plan } = JSON.parse(e.data);
|
|
2614
|
+
if (drillFeatureKey === featureKey) {
|
|
2615
|
+
e2ePlan = plan;
|
|
2616
|
+
if (drillTestType === 'e2e') renderContent();
|
|
2120
2617
|
}
|
|
2121
|
-
document.getElementById('agentPanelStatus').textContent =
|
|
2122
|
-
data.failed === 0 ? `✅ E2E: ${data.passed} passed` : `⚠️ E2E: ${data.passed}/${data.failed}`;
|
|
2123
2618
|
});
|
|
2124
2619
|
|
|
2125
2620
|
es.onerror = () => {
|
|
@@ -2129,7 +2624,15 @@ function connectSSE() {
|
|
|
2129
2624
|
};
|
|
2130
2625
|
}
|
|
2131
2626
|
|
|
2132
|
-
init().then(() =>
|
|
2627
|
+
init().then(() => {
|
|
2628
|
+
restoreSessions();
|
|
2629
|
+
connectSSE();
|
|
2630
|
+
// Sync content padding with terminal panel open state automatically
|
|
2631
|
+
new MutationObserver(() => {
|
|
2632
|
+
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
2633
|
+
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
2634
|
+
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
2635
|
+
});
|
|
2133
2636
|
</script>
|
|
2134
2637
|
</body>
|
|
2135
2638
|
</html>
|