viberadar 0.3.64 → 0.3.66
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 +2 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +31 -7
- package/dist/scanner/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +207 -4
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +1429 -1279
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -191,7 +191,7 @@
|
|
|
191
191
|
.obs-priority-low { color: var(--green); }
|
|
192
192
|
.obs-catalog { margin-top:10px; background: var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px; }
|
|
193
193
|
.obs-catalog h4 { font-size:12px; margin-bottom:6px; }
|
|
194
|
-
.obs-cat-row { display:grid; grid-template-columns: 1.
|
|
194
|
+
.obs-cat-row { display:grid; grid-template-columns: 1.4fr .5fr .6fr .4fr 1.2fr .7fr auto; gap:6px; font-size:11px; color:var(--muted); padding:5px 0; border-bottom:1px dashed var(--border); }
|
|
195
195
|
.obs-cat-row.head { color: var(--text); font-weight:600; text-transform:uppercase; font-size:10px; }
|
|
196
196
|
|
|
197
197
|
/* ── Layout ──────────────────────────────────────────────────────────────── */
|
|
@@ -611,19 +611,19 @@
|
|
|
611
611
|
.tt-fix-btn:hover { background: rgba(255,200,0,0.1); color: var(--yellow); border-color: var(--yellow); }
|
|
612
612
|
.tt-write-btn { border-color: var(--accent); color: var(--accent); }
|
|
613
613
|
.tt-write-btn:hover { background: rgba(88,166,255,0.1); color: var(--accent); border-color: var(--accent); }
|
|
614
|
-
.file-rows { display: flex; flex-direction: column; gap: 2px; contain: content; }
|
|
615
|
-
.file-row {
|
|
616
|
-
display: flex;
|
|
617
|
-
align-items: center;
|
|
618
|
-
gap: 8px;
|
|
619
|
-
padding: 7px 10px;
|
|
614
|
+
.file-rows { display: flex; flex-direction: column; gap: 2px; contain: content; }
|
|
615
|
+
.file-row {
|
|
616
|
+
display: flex;
|
|
617
|
+
align-items: center;
|
|
618
|
+
gap: 8px;
|
|
619
|
+
padding: 7px 10px;
|
|
620
620
|
border-radius: 6px;
|
|
621
621
|
cursor: pointer;
|
|
622
|
-
font-size: 13px;
|
|
623
|
-
transition: background 0.1s;
|
|
624
|
-
content-visibility: auto;
|
|
625
|
-
contain-intrinsic-size: 34px;
|
|
626
|
-
}
|
|
622
|
+
font-size: 13px;
|
|
623
|
+
transition: background 0.1s;
|
|
624
|
+
content-visibility: auto;
|
|
625
|
+
contain-intrinsic-size: 34px;
|
|
626
|
+
}
|
|
627
627
|
.file-row:hover { background: var(--bg-card); }
|
|
628
628
|
.file-row.active { background: var(--bg-hover); border-left: 2px solid var(--blue); padding-left: 8px; }
|
|
629
629
|
.file-row-icon { font-size: 12px; flex-shrink: 0; }
|
|
@@ -659,6 +659,16 @@
|
|
|
659
659
|
color: #fff !important; font-weight: 600;
|
|
660
660
|
}
|
|
661
661
|
.file-row-fix-btn:hover { opacity: 0.85; }
|
|
662
|
+
.obs-action-btn {
|
|
663
|
+
display: inline-flex; align-items: center; gap: 3px;
|
|
664
|
+
padding: 2px 7px; font-size: 10px;
|
|
665
|
+
background: transparent; border: 1px solid var(--border); border-radius: 4px;
|
|
666
|
+
color: var(--dim); cursor: pointer; white-space: nowrap; flex-shrink: 0;
|
|
667
|
+
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
668
|
+
}
|
|
669
|
+
.obs-action-btn:hover { background: var(--accent); color: #000; border-color: var(--accent); }
|
|
670
|
+
.obs-batch-btn { border-color: var(--yellow); color: var(--yellow); }
|
|
671
|
+
.obs-batch-btn:hover { background: rgba(255,200,0,0.15); color: #000; border-color: var(--yellow); }
|
|
662
672
|
.file-row-err-badge {
|
|
663
673
|
display: inline-flex; align-items: center;
|
|
664
674
|
font-size: 11px; padding: 1px 6px; border-radius: 10px;
|
|
@@ -675,12 +685,12 @@
|
|
|
675
685
|
.file-agent-spinner.running { border: 2px solid var(--yellow); border-top-color: transparent; animation: spin 0.7s linear infinite; }
|
|
676
686
|
.file-agent-spinner.queued { border: 2px solid var(--dim); border-top-color: transparent; animation: spin 1.5s linear infinite; }
|
|
677
687
|
@keyframes spin { to { transform: rotate(360deg); } }
|
|
678
|
-
.file-row-errors {
|
|
679
|
-
padding: 4px 10px 6px 32px;
|
|
680
|
-
display: flex; flex-direction: column; gap: 3px;
|
|
681
|
-
content-visibility: auto;
|
|
682
|
-
contain-intrinsic-size: 46px;
|
|
683
|
-
}
|
|
688
|
+
.file-row-errors {
|
|
689
|
+
padding: 4px 10px 6px 32px;
|
|
690
|
+
display: flex; flex-direction: column; gap: 3px;
|
|
691
|
+
content-visibility: auto;
|
|
692
|
+
contain-intrinsic-size: 46px;
|
|
693
|
+
}
|
|
684
694
|
.err-item { display: flex; flex-direction: column; gap: 1px; }
|
|
685
695
|
.err-name { font-size: 11px; color: var(--muted); }
|
|
686
696
|
.err-msg { font-size: 10px; color: var(--red); font-family: monospace; opacity: 0.85; }
|
|
@@ -769,92 +779,92 @@
|
|
|
769
779
|
cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
770
780
|
}
|
|
771
781
|
.agent-panel-cancel:hover { background: var(--yellow); color: #000; }
|
|
772
|
-
.agent-queue-badge {
|
|
773
|
-
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
774
|
-
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
775
|
-
}
|
|
776
|
-
.agent-toolbar {
|
|
777
|
-
display: flex;
|
|
778
|
-
align-items: center;
|
|
779
|
-
gap: 8px;
|
|
780
|
-
padding: 6px 12px;
|
|
781
|
-
background: #0b1018;
|
|
782
|
-
border-bottom: 1px solid var(--border);
|
|
783
|
-
flex-wrap: wrap;
|
|
784
|
-
}
|
|
785
|
-
.agent-toolbar input[type="text"] {
|
|
786
|
-
min-width: 220px;
|
|
787
|
-
padding: 4px 8px;
|
|
788
|
-
border-radius: 4px;
|
|
789
|
-
border: 1px solid var(--border);
|
|
790
|
-
background: #0b1220;
|
|
791
|
-
color: var(--text);
|
|
792
|
-
font-size: 11px;
|
|
793
|
-
}
|
|
794
|
-
.agent-toolbar label {
|
|
795
|
-
display: inline-flex;
|
|
796
|
-
align-items: center;
|
|
797
|
-
gap: 5px;
|
|
798
|
-
font-size: 11px;
|
|
799
|
-
color: var(--muted);
|
|
800
|
-
user-select: none;
|
|
801
|
-
}
|
|
802
|
-
.agent-toolbar-btn {
|
|
803
|
-
background: none;
|
|
804
|
-
border: 1px solid var(--border);
|
|
805
|
-
color: var(--muted);
|
|
806
|
-
cursor: pointer;
|
|
807
|
-
font-size: 11px;
|
|
808
|
-
padding: 3px 8px;
|
|
809
|
-
border-radius: 4px;
|
|
810
|
-
}
|
|
811
|
-
.agent-toolbar-btn:hover { color: var(--text); border-color: var(--blue); }
|
|
812
|
-
.agent-toolbar-btn:disabled {
|
|
813
|
-
opacity: 0.45;
|
|
814
|
-
cursor: not-allowed;
|
|
815
|
-
border-color: var(--border);
|
|
816
|
-
color: var(--dim);
|
|
817
|
-
}
|
|
818
|
-
.agent-toolbar-meta { font-size: 11px; color: var(--dim); margin-left: auto; }
|
|
819
|
-
.agent-queue-panel {
|
|
820
|
-
display: none;
|
|
821
|
-
border-bottom: 1px solid var(--border);
|
|
822
|
-
background: #090f18;
|
|
823
|
-
padding: 8px 12px;
|
|
824
|
-
max-height: 110px;
|
|
825
|
-
overflow-y: auto;
|
|
826
|
-
}
|
|
827
|
-
.agent-queue-title {
|
|
828
|
-
font-size: 11px;
|
|
829
|
-
color: var(--muted);
|
|
830
|
-
margin-bottom: 6px;
|
|
831
|
-
font-weight: 600;
|
|
832
|
-
}
|
|
833
|
-
.agent-queue-item {
|
|
834
|
-
display: flex;
|
|
835
|
-
align-items: center;
|
|
836
|
-
gap: 8px;
|
|
837
|
-
font-size: 11px;
|
|
838
|
-
color: var(--text);
|
|
839
|
-
margin-bottom: 4px;
|
|
840
|
-
}
|
|
841
|
-
.agent-queue-item:last-child { margin-bottom: 0; }
|
|
842
|
-
.agent-queue-pos {
|
|
843
|
-
color: var(--dim);
|
|
844
|
-
min-width: 24px;
|
|
845
|
-
}
|
|
846
|
-
.agent-queue-actions { margin-left: auto; display: inline-flex; gap: 4px; }
|
|
847
|
-
.agent-queue-action {
|
|
848
|
-
border: 1px solid var(--border);
|
|
849
|
-
background: transparent;
|
|
850
|
-
color: var(--muted);
|
|
851
|
-
border-radius: 4px;
|
|
852
|
-
padding: 1px 6px;
|
|
853
|
-
cursor: pointer;
|
|
854
|
-
font-size: 10px;
|
|
855
|
-
line-height: 1.4;
|
|
856
|
-
}
|
|
857
|
-
.agent-queue-action:hover { border-color: var(--blue); color: var(--text); }
|
|
782
|
+
.agent-queue-badge {
|
|
783
|
+
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
784
|
+
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
785
|
+
}
|
|
786
|
+
.agent-toolbar {
|
|
787
|
+
display: flex;
|
|
788
|
+
align-items: center;
|
|
789
|
+
gap: 8px;
|
|
790
|
+
padding: 6px 12px;
|
|
791
|
+
background: #0b1018;
|
|
792
|
+
border-bottom: 1px solid var(--border);
|
|
793
|
+
flex-wrap: wrap;
|
|
794
|
+
}
|
|
795
|
+
.agent-toolbar input[type="text"] {
|
|
796
|
+
min-width: 220px;
|
|
797
|
+
padding: 4px 8px;
|
|
798
|
+
border-radius: 4px;
|
|
799
|
+
border: 1px solid var(--border);
|
|
800
|
+
background: #0b1220;
|
|
801
|
+
color: var(--text);
|
|
802
|
+
font-size: 11px;
|
|
803
|
+
}
|
|
804
|
+
.agent-toolbar label {
|
|
805
|
+
display: inline-flex;
|
|
806
|
+
align-items: center;
|
|
807
|
+
gap: 5px;
|
|
808
|
+
font-size: 11px;
|
|
809
|
+
color: var(--muted);
|
|
810
|
+
user-select: none;
|
|
811
|
+
}
|
|
812
|
+
.agent-toolbar-btn {
|
|
813
|
+
background: none;
|
|
814
|
+
border: 1px solid var(--border);
|
|
815
|
+
color: var(--muted);
|
|
816
|
+
cursor: pointer;
|
|
817
|
+
font-size: 11px;
|
|
818
|
+
padding: 3px 8px;
|
|
819
|
+
border-radius: 4px;
|
|
820
|
+
}
|
|
821
|
+
.agent-toolbar-btn:hover { color: var(--text); border-color: var(--blue); }
|
|
822
|
+
.agent-toolbar-btn:disabled {
|
|
823
|
+
opacity: 0.45;
|
|
824
|
+
cursor: not-allowed;
|
|
825
|
+
border-color: var(--border);
|
|
826
|
+
color: var(--dim);
|
|
827
|
+
}
|
|
828
|
+
.agent-toolbar-meta { font-size: 11px; color: var(--dim); margin-left: auto; }
|
|
829
|
+
.agent-queue-panel {
|
|
830
|
+
display: none;
|
|
831
|
+
border-bottom: 1px solid var(--border);
|
|
832
|
+
background: #090f18;
|
|
833
|
+
padding: 8px 12px;
|
|
834
|
+
max-height: 110px;
|
|
835
|
+
overflow-y: auto;
|
|
836
|
+
}
|
|
837
|
+
.agent-queue-title {
|
|
838
|
+
font-size: 11px;
|
|
839
|
+
color: var(--muted);
|
|
840
|
+
margin-bottom: 6px;
|
|
841
|
+
font-weight: 600;
|
|
842
|
+
}
|
|
843
|
+
.agent-queue-item {
|
|
844
|
+
display: flex;
|
|
845
|
+
align-items: center;
|
|
846
|
+
gap: 8px;
|
|
847
|
+
font-size: 11px;
|
|
848
|
+
color: var(--text);
|
|
849
|
+
margin-bottom: 4px;
|
|
850
|
+
}
|
|
851
|
+
.agent-queue-item:last-child { margin-bottom: 0; }
|
|
852
|
+
.agent-queue-pos {
|
|
853
|
+
color: var(--dim);
|
|
854
|
+
min-width: 24px;
|
|
855
|
+
}
|
|
856
|
+
.agent-queue-actions { margin-left: auto; display: inline-flex; gap: 4px; }
|
|
857
|
+
.agent-queue-action {
|
|
858
|
+
border: 1px solid var(--border);
|
|
859
|
+
background: transparent;
|
|
860
|
+
color: var(--muted);
|
|
861
|
+
border-radius: 4px;
|
|
862
|
+
padding: 1px 6px;
|
|
863
|
+
cursor: pointer;
|
|
864
|
+
font-size: 10px;
|
|
865
|
+
line-height: 1.4;
|
|
866
|
+
}
|
|
867
|
+
.agent-queue-action:hover { border-color: var(--blue); color: var(--text); }
|
|
858
868
|
/* ── Console Tabs ───────────────────────────────────────────────────────── */
|
|
859
869
|
.agent-tabs-bar {
|
|
860
870
|
display: flex; align-items: stretch; overflow-x: auto;
|
|
@@ -934,50 +944,50 @@
|
|
|
934
944
|
border-color: var(--accent);
|
|
935
945
|
color: var(--accent);
|
|
936
946
|
}
|
|
937
|
-
.bulk-actions-btn:hover:not(:disabled) { background: var(--bg-hover); }
|
|
938
|
-
.bulk-actions-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
939
|
-
.feature-file-filters {
|
|
940
|
-
display: flex;
|
|
941
|
-
align-items: center;
|
|
942
|
-
gap: 8px;
|
|
943
|
-
margin: 0 0 10px;
|
|
944
|
-
}
|
|
945
|
-
.feature-filter-toggle {
|
|
946
|
-
display: inline-flex;
|
|
947
|
-
align-items: center;
|
|
948
|
-
gap: 8px;
|
|
949
|
-
padding: 6px 10px;
|
|
950
|
-
border-radius: 6px;
|
|
951
|
-
border: 1px solid var(--border);
|
|
952
|
-
background: var(--bg-card);
|
|
953
|
-
color: var(--muted);
|
|
954
|
-
font-size: 12px;
|
|
955
|
-
cursor: pointer;
|
|
956
|
-
user-select: none;
|
|
957
|
-
}
|
|
958
|
-
.feature-filter-toggle:hover { background: var(--bg-hover); color: var(--text); }
|
|
959
|
-
.feature-filter-toggle input {
|
|
960
|
-
width: 14px;
|
|
961
|
-
height: 14px;
|
|
962
|
-
accent-color: var(--blue);
|
|
963
|
-
cursor: pointer;
|
|
964
|
-
}
|
|
965
|
-
.feature-filter-meta { color: var(--dim); font-size: 11px; }
|
|
966
|
-
.agent-terminal {
|
|
967
|
-
flex: 1;
|
|
968
|
-
overflow-y: auto;
|
|
969
|
-
padding: 10px 16px;
|
|
970
|
-
font-family: 'Consolas', 'Menlo', 'Courier New', monospace;
|
|
947
|
+
.bulk-actions-btn:hover:not(:disabled) { background: var(--bg-hover); }
|
|
948
|
+
.bulk-actions-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
949
|
+
.feature-file-filters {
|
|
950
|
+
display: flex;
|
|
951
|
+
align-items: center;
|
|
952
|
+
gap: 8px;
|
|
953
|
+
margin: 0 0 10px;
|
|
954
|
+
}
|
|
955
|
+
.feature-filter-toggle {
|
|
956
|
+
display: inline-flex;
|
|
957
|
+
align-items: center;
|
|
958
|
+
gap: 8px;
|
|
959
|
+
padding: 6px 10px;
|
|
960
|
+
border-radius: 6px;
|
|
961
|
+
border: 1px solid var(--border);
|
|
962
|
+
background: var(--bg-card);
|
|
963
|
+
color: var(--muted);
|
|
964
|
+
font-size: 12px;
|
|
965
|
+
cursor: pointer;
|
|
966
|
+
user-select: none;
|
|
967
|
+
}
|
|
968
|
+
.feature-filter-toggle:hover { background: var(--bg-hover); color: var(--text); }
|
|
969
|
+
.feature-filter-toggle input {
|
|
970
|
+
width: 14px;
|
|
971
|
+
height: 14px;
|
|
972
|
+
accent-color: var(--blue);
|
|
973
|
+
cursor: pointer;
|
|
974
|
+
}
|
|
975
|
+
.feature-filter-meta { color: var(--dim); font-size: 11px; }
|
|
976
|
+
.agent-terminal {
|
|
977
|
+
flex: 1;
|
|
978
|
+
overflow-y: auto;
|
|
979
|
+
padding: 10px 16px;
|
|
980
|
+
font-family: 'Consolas', 'Menlo', 'Courier New', monospace;
|
|
971
981
|
font-size: 12px;
|
|
972
982
|
line-height: 1.5;
|
|
973
983
|
}
|
|
974
|
-
.agent-line { color: #c9d1d9; }
|
|
975
|
-
.agent-line.err { color: var(--red); }
|
|
976
|
-
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
977
|
-
.agent-line.match { background: rgba(31, 111, 235, 0.22); }
|
|
978
|
-
.agent-line.command { color: #79c0ff; }
|
|
979
|
-
|
|
980
|
-
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
984
|
+
.agent-line { color: #c9d1d9; }
|
|
985
|
+
.agent-line.err { color: var(--red); }
|
|
986
|
+
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
987
|
+
.agent-line.match { background: rgba(31, 111, 235, 0.22); }
|
|
988
|
+
.agent-line.command { color: #79c0ff; }
|
|
989
|
+
|
|
990
|
+
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
981
991
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
982
992
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
983
993
|
/* ─── E2E Plan UI ─────────────────────────────────────────────────── */
|
|
@@ -1089,34 +1099,34 @@
|
|
|
1089
1099
|
<div id="panelContent"></div>
|
|
1090
1100
|
</div>
|
|
1091
1101
|
|
|
1092
|
-
<div class="agent-panel" id="agentPanel">
|
|
1093
|
-
<div class="agent-panel-header">
|
|
1094
|
-
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
1095
|
-
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
1102
|
+
<div class="agent-panel" id="agentPanel">
|
|
1103
|
+
<div class="agent-panel-header">
|
|
1104
|
+
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
1105
|
+
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
1096
1106
|
<span class="agent-queue-badge" id="agentQueueBadge" style="display:none">📋 <span id="agentQueueCount">0</span> в очереди</span>
|
|
1097
1107
|
<button class="agent-panel-cancel" id="agentQueueClearBtn" onclick="clearAgentQueue()" title="Очистить очередь" style="display:none">🗑 очередь</button>
|
|
1098
1108
|
<button class="agent-panel-cancel" id="agentCancelBtn" onclick="cancelAgent()" title="Сбросить состояние агента" style="display:none">⏹ сброс</button>
|
|
1099
|
-
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
1100
|
-
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
1101
|
-
</div>
|
|
1102
|
-
<div class="agent-toolbar">
|
|
1103
|
-
<input id="agentSearchInput" type="text" placeholder="Поиск по терминалу..." />
|
|
1104
|
-
<label><input id="agentSearchErrorsOnly" type="checkbox" /> only errors</label>
|
|
1105
|
-
<label><input id="agentSearchCurrentRunOnly" type="checkbox" /> current run</label>
|
|
1106
|
-
<label><input id="agentSearchRegex" type="checkbox" /> regex</label>
|
|
1107
|
-
<button class="agent-toolbar-btn" id="btnMatchPrev" onclick="jumpTerminalMatch(-1)">↑ match</button>
|
|
1108
|
-
<button class="agent-toolbar-btn" id="btnMatchNext" onclick="jumpTerminalMatch(1)">↓ match</button>
|
|
1109
|
-
<button class="agent-toolbar-btn" id="btnCmdPrev" onclick="jumpCommand(-1)">↑ command</button>
|
|
1110
|
-
<button class="agent-toolbar-btn" id="btnCmdNext" onclick="jumpCommand(1)">↓ command</button>
|
|
1111
|
-
<button class="agent-toolbar-btn" id="btnErrPrev" onclick="jumpError(-1)">↑ error</button>
|
|
1112
|
-
<button class="agent-toolbar-btn" id="btnErrNext" onclick="jumpError(1)">↓ error</button>
|
|
1113
|
-
<button class="agent-toolbar-btn" id="btnExportRun" onclick="exportActiveRun()">Export run</button>
|
|
1114
|
-
<span class="agent-toolbar-meta" id="agentSearchMeta">0 matches</span>
|
|
1115
|
-
</div>
|
|
1116
|
-
<div class="agent-queue-panel" id="agentQueuePanel"></div>
|
|
1117
|
-
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
1118
|
-
<div class="agent-terminal" id="agentTerminal"></div>
|
|
1119
|
-
</div>
|
|
1109
|
+
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
1110
|
+
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
1111
|
+
</div>
|
|
1112
|
+
<div class="agent-toolbar">
|
|
1113
|
+
<input id="agentSearchInput" type="text" placeholder="Поиск по терминалу..." />
|
|
1114
|
+
<label><input id="agentSearchErrorsOnly" type="checkbox" /> only errors</label>
|
|
1115
|
+
<label><input id="agentSearchCurrentRunOnly" type="checkbox" /> current run</label>
|
|
1116
|
+
<label><input id="agentSearchRegex" type="checkbox" /> regex</label>
|
|
1117
|
+
<button class="agent-toolbar-btn" id="btnMatchPrev" onclick="jumpTerminalMatch(-1)">↑ match</button>
|
|
1118
|
+
<button class="agent-toolbar-btn" id="btnMatchNext" onclick="jumpTerminalMatch(1)">↓ match</button>
|
|
1119
|
+
<button class="agent-toolbar-btn" id="btnCmdPrev" onclick="jumpCommand(-1)">↑ command</button>
|
|
1120
|
+
<button class="agent-toolbar-btn" id="btnCmdNext" onclick="jumpCommand(1)">↓ command</button>
|
|
1121
|
+
<button class="agent-toolbar-btn" id="btnErrPrev" onclick="jumpError(-1)">↑ error</button>
|
|
1122
|
+
<button class="agent-toolbar-btn" id="btnErrNext" onclick="jumpError(1)">↓ error</button>
|
|
1123
|
+
<button class="agent-toolbar-btn" id="btnExportRun" onclick="exportActiveRun()">Export run</button>
|
|
1124
|
+
<span class="agent-toolbar-meta" id="agentSearchMeta">0 matches</span>
|
|
1125
|
+
</div>
|
|
1126
|
+
<div class="agent-queue-panel" id="agentQueuePanel"></div>
|
|
1127
|
+
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
1128
|
+
<div class="agent-terminal" id="agentTerminal"></div>
|
|
1129
|
+
</div>
|
|
1120
1130
|
|
|
1121
1131
|
<script>
|
|
1122
1132
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
@@ -1125,21 +1135,21 @@ let contextMode = 'qa';
|
|
|
1125
1135
|
let view = 'features';
|
|
1126
1136
|
let searchQuery = '';
|
|
1127
1137
|
let activeTypes = new Set();
|
|
1128
|
-
let activePanelKey = null;
|
|
1129
|
-
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
1130
|
-
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
1131
|
-
let showOnlyUntestedInFeature = false; // source tab in feature detail
|
|
1132
|
-
const selectedSourceFiles = new Set(); // normalized relative paths for batch actions
|
|
1133
|
-
const FILE_ROWS_INITIAL_LIMIT = 250;
|
|
1134
|
-
const FILE_ROWS_LIMIT_STEP = 250;
|
|
1135
|
-
let fileRowsRenderKey = '';
|
|
1136
|
-
let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1137
|
-
let e2ePlan = null; // current E2E plan object
|
|
1138
|
-
let e2ePlanLoading = false;
|
|
1139
|
-
const modeStore = {
|
|
1140
|
-
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1141
|
-
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1142
|
-
};
|
|
1138
|
+
let activePanelKey = null;
|
|
1139
|
+
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
1140
|
+
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
1141
|
+
let showOnlyUntestedInFeature = false; // source tab in feature detail
|
|
1142
|
+
const selectedSourceFiles = new Set(); // normalized relative paths for batch actions
|
|
1143
|
+
const FILE_ROWS_INITIAL_LIMIT = 250;
|
|
1144
|
+
const FILE_ROWS_LIMIT_STEP = 250;
|
|
1145
|
+
let fileRowsRenderKey = '';
|
|
1146
|
+
let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1147
|
+
let e2ePlan = null; // current E2E plan object
|
|
1148
|
+
let e2ePlanLoading = false;
|
|
1149
|
+
const modeStore = {
|
|
1150
|
+
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1151
|
+
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1152
|
+
};
|
|
1143
1153
|
|
|
1144
1154
|
function getModeFromPath(pathname = window.location.pathname) {
|
|
1145
1155
|
if (pathname.startsWith('/radar/observability')) return 'observability';
|
|
@@ -1160,31 +1170,31 @@ function saveModeState(mode) {
|
|
|
1160
1170
|
modeStore[mode] = {
|
|
1161
1171
|
view,
|
|
1162
1172
|
searchQuery,
|
|
1163
|
-
activeTypes: new Set(activeTypes),
|
|
1164
|
-
drillFeatureKey,
|
|
1165
|
-
drillTestType,
|
|
1166
|
-
activePanelKey,
|
|
1167
|
-
showOnlyUntestedInFeature,
|
|
1168
|
-
};
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
function restoreModeState(mode) {
|
|
1172
|
-
const state = modeStore[mode];
|
|
1173
|
+
activeTypes: new Set(activeTypes),
|
|
1174
|
+
drillFeatureKey,
|
|
1175
|
+
drillTestType,
|
|
1176
|
+
activePanelKey,
|
|
1177
|
+
showOnlyUntestedInFeature,
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
|
|
1181
|
+
function restoreModeState(mode) {
|
|
1182
|
+
const state = modeStore[mode];
|
|
1173
1183
|
view = state.view;
|
|
1174
1184
|
searchQuery = state.searchQuery;
|
|
1175
|
-
activeTypes = new Set(state.activeTypes);
|
|
1176
|
-
drillFeatureKey = state.drillFeatureKey;
|
|
1177
|
-
drillTestType = state.drillTestType;
|
|
1178
|
-
activePanelKey = state.activePanelKey;
|
|
1179
|
-
showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
|
|
1180
|
-
}
|
|
1185
|
+
activeTypes = new Set(state.activeTypes);
|
|
1186
|
+
drillFeatureKey = state.drillFeatureKey;
|
|
1187
|
+
drillTestType = state.drillTestType;
|
|
1188
|
+
activePanelKey = state.activePanelKey;
|
|
1189
|
+
showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
|
|
1190
|
+
}
|
|
1181
1191
|
|
|
1182
1192
|
function switchMode(nextMode) {
|
|
1183
1193
|
if (nextMode === contextMode) return;
|
|
1184
1194
|
saveModeState(contextMode);
|
|
1185
1195
|
contextMode = nextMode;
|
|
1186
1196
|
restoreModeState(contextMode);
|
|
1187
|
-
if (contextMode === 'observability') {
|
|
1197
|
+
if (contextMode === 'observability') {
|
|
1188
1198
|
view = 'files';
|
|
1189
1199
|
drillFeatureKey = null;
|
|
1190
1200
|
drillTestType = null;
|
|
@@ -1198,11 +1208,11 @@ function switchMode(nextMode) {
|
|
|
1198
1208
|
renderSidebar();
|
|
1199
1209
|
renderContent();
|
|
1200
1210
|
}
|
|
1201
|
-
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1202
|
-
let runAllRunning = false;
|
|
1203
|
-
let refreshDataInFlight = false;
|
|
1204
|
-
let refreshDataQueued = false;
|
|
1205
|
-
let refreshDataTimer = null;
|
|
1211
|
+
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1212
|
+
let runAllRunning = false;
|
|
1213
|
+
let refreshDataInFlight = false;
|
|
1214
|
+
let refreshDataQueued = false;
|
|
1215
|
+
let refreshDataTimer = null;
|
|
1206
1216
|
|
|
1207
1217
|
function escapeHtml(text) {
|
|
1208
1218
|
return String(text || '')
|
|
@@ -1241,42 +1251,42 @@ function setFeatureDrill(featureKey, syncHash = true) {
|
|
|
1241
1251
|
renderContent();
|
|
1242
1252
|
}
|
|
1243
1253
|
|
|
1244
|
-
function selectedFilesForFeature(featureKey, visibleSourceFiles = null) {
|
|
1245
|
-
const visibleSet = visibleSourceFiles ? new Set(visibleSourceFiles.map(p => p.replace(/\\/g, '/'))) : null;
|
|
1246
|
-
return Array.from(selectedSourceFiles).filter((relPath) => {
|
|
1254
|
+
function selectedFilesForFeature(featureKey, visibleSourceFiles = null) {
|
|
1255
|
+
const visibleSet = visibleSourceFiles ? new Set(visibleSourceFiles.map(p => p.replace(/\\/g, '/'))) : null;
|
|
1256
|
+
return Array.from(selectedSourceFiles).filter((relPath) => {
|
|
1247
1257
|
if (visibleSet && !visibleSet.has(relPath)) return false;
|
|
1248
1258
|
const mod = D?.modules?.find((m) => m.relativePath.replace(/\\/g, '/') === relPath);
|
|
1249
|
-
return !!mod && mod.type !== 'test' && mod.featureKeys?.includes(featureKey);
|
|
1250
|
-
});
|
|
1251
|
-
}
|
|
1252
|
-
|
|
1253
|
-
function getFileRowsWindow(items, renderKey) {
|
|
1254
|
-
if (fileRowsRenderKey !== renderKey) {
|
|
1255
|
-
fileRowsRenderKey = renderKey;
|
|
1256
|
-
fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1257
|
-
}
|
|
1258
|
-
const visibleRows = items.slice(0, fileRowsRenderLimit);
|
|
1259
|
-
const hiddenRows = Math.max(0, items.length - visibleRows.length);
|
|
1260
|
-
return { visibleRows, hiddenRows, hasMoreRows: hiddenRows > 0 };
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
function increaseFileRowsLimit() {
|
|
1264
|
-
fileRowsRenderLimit += FILE_ROWS_LIMIT_STEP;
|
|
1265
|
-
}
|
|
1266
|
-
|
|
1267
|
-
function bindFileRowsClick(container) {
|
|
1268
|
-
const fileRows = container.querySelector('#fileRows');
|
|
1269
|
-
if (!fileRows) return;
|
|
1270
|
-
fileRows.onclick = (event) => {
|
|
1271
|
-
const row = event.target.closest('.file-row[data-id]');
|
|
1272
|
-
if (!row) return;
|
|
1273
|
-
const moduleId = row.dataset.id;
|
|
1274
|
-
const mod = D.modules.find((m) => String(m.id) === moduleId);
|
|
1275
|
-
if (mod) openModulePanel(mod);
|
|
1276
|
-
};
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function applyHashRoute() {
|
|
1259
|
+
return !!mod && mod.type !== 'test' && mod.featureKeys?.includes(featureKey);
|
|
1260
|
+
});
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1263
|
+
function getFileRowsWindow(items, renderKey) {
|
|
1264
|
+
if (fileRowsRenderKey !== renderKey) {
|
|
1265
|
+
fileRowsRenderKey = renderKey;
|
|
1266
|
+
fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1267
|
+
}
|
|
1268
|
+
const visibleRows = items.slice(0, fileRowsRenderLimit);
|
|
1269
|
+
const hiddenRows = Math.max(0, items.length - visibleRows.length);
|
|
1270
|
+
return { visibleRows, hiddenRows, hasMoreRows: hiddenRows > 0 };
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
function increaseFileRowsLimit() {
|
|
1274
|
+
fileRowsRenderLimit += FILE_ROWS_LIMIT_STEP;
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
function bindFileRowsClick(container) {
|
|
1278
|
+
const fileRows = container.querySelector('#fileRows');
|
|
1279
|
+
if (!fileRows) return;
|
|
1280
|
+
fileRows.onclick = (event) => {
|
|
1281
|
+
const row = event.target.closest('.file-row[data-id]');
|
|
1282
|
+
if (!row) return;
|
|
1283
|
+
const moduleId = row.dataset.id;
|
|
1284
|
+
const mod = D.modules.find((m) => String(m.id) === moduleId);
|
|
1285
|
+
if (mod) openModulePanel(mod);
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function applyHashRoute() {
|
|
1280
1290
|
const hash = (window.location.hash || '').replace(/^#/, '');
|
|
1281
1291
|
if (!hash) {
|
|
1282
1292
|
if (view === 'features' && drillFeatureKey) {
|
|
@@ -1324,145 +1334,145 @@ async function runAllTests() {
|
|
|
1324
1334
|
}
|
|
1325
1335
|
|
|
1326
1336
|
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
1327
|
-
let agentRunning = false;
|
|
1328
|
-
const agentRunningPaths = new Set(); // paths of files currently being processed by agent
|
|
1329
|
-
const agentQueuedPaths = new Set(); // paths of files waiting in queue
|
|
1330
|
-
let agentQueueState = [];
|
|
1331
|
-
let agentRunsState = [];
|
|
1332
|
-
let agentActiveRun = null;
|
|
1333
|
-
let currentRunId = null;
|
|
1334
|
-
|
|
1335
|
-
// ─── Console Sessions ─────────────────────────────────────────────────────────
|
|
1336
|
-
const consoleSessions = []; // { id, title, lines, status, startTime }
|
|
1337
|
-
let activeSessionId = null; // currently viewed tab
|
|
1338
|
-
let runningSessionId = null; // tab that is currently receiving output
|
|
1339
|
-
const runSessionMap = new Map(); // runId -> sessionId
|
|
1340
|
-
const sessionRunMap = new Map(); // sessionId -> runId
|
|
1341
|
-
const SESSION_MAX = 25;
|
|
1342
|
-
const SESSION_LINE_LIMIT = 3000;
|
|
1343
|
-
const SESSIONS_KEY = 'viberadar_sessions';
|
|
1344
|
-
let terminalSearchQuery = '';
|
|
1345
|
-
let terminalSearchErrorsOnly = false;
|
|
1346
|
-
let terminalSearchCurrentRunOnly = false;
|
|
1347
|
-
let terminalSearchRegex = false;
|
|
1348
|
-
let terminalMatchRefs = [];
|
|
1349
|
-
let terminalCommandRefs = [];
|
|
1350
|
-
let terminalErrorRefs = [];
|
|
1351
|
-
let terminalMatchCursor = -1;
|
|
1352
|
-
let terminalCommandCursor = -1;
|
|
1353
|
-
let terminalErrorCursor = -1;
|
|
1337
|
+
let agentRunning = false;
|
|
1338
|
+
const agentRunningPaths = new Set(); // paths of files currently being processed by agent
|
|
1339
|
+
const agentQueuedPaths = new Set(); // paths of files waiting in queue
|
|
1340
|
+
let agentQueueState = [];
|
|
1341
|
+
let agentRunsState = [];
|
|
1342
|
+
let agentActiveRun = null;
|
|
1343
|
+
let currentRunId = null;
|
|
1344
|
+
|
|
1345
|
+
// ─── Console Sessions ─────────────────────────────────────────────────────────
|
|
1346
|
+
const consoleSessions = []; // { id, title, lines, status, startTime }
|
|
1347
|
+
let activeSessionId = null; // currently viewed tab
|
|
1348
|
+
let runningSessionId = null; // tab that is currently receiving output
|
|
1349
|
+
const runSessionMap = new Map(); // runId -> sessionId
|
|
1350
|
+
const sessionRunMap = new Map(); // sessionId -> runId
|
|
1351
|
+
const SESSION_MAX = 25;
|
|
1352
|
+
const SESSION_LINE_LIMIT = 3000;
|
|
1353
|
+
const SESSIONS_KEY = 'viberadar_sessions';
|
|
1354
|
+
let terminalSearchQuery = '';
|
|
1355
|
+
let terminalSearchErrorsOnly = false;
|
|
1356
|
+
let terminalSearchCurrentRunOnly = false;
|
|
1357
|
+
let terminalSearchRegex = false;
|
|
1358
|
+
let terminalMatchRefs = [];
|
|
1359
|
+
let terminalCommandRefs = [];
|
|
1360
|
+
let terminalErrorRefs = [];
|
|
1361
|
+
let terminalMatchCursor = -1;
|
|
1362
|
+
let terminalCommandCursor = -1;
|
|
1363
|
+
let terminalErrorCursor = -1;
|
|
1354
1364
|
|
|
1355
1365
|
function _sessionId() {
|
|
1356
1366
|
return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
1357
1367
|
}
|
|
1358
1368
|
|
|
1359
|
-
function saveSessions() {
|
|
1360
|
-
try {
|
|
1361
|
-
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
1362
|
-
...s,
|
|
1363
|
-
runId: sessionRunMap.get(s.id) || s.runId || null,
|
|
1364
|
-
lines: s.lines.slice(-500)
|
|
1365
|
-
}));
|
|
1366
|
-
localStorage.setItem(SESSIONS_KEY, JSON.stringify(data));
|
|
1367
|
-
} catch {}
|
|
1368
|
-
}
|
|
1369
|
+
function saveSessions() {
|
|
1370
|
+
try {
|
|
1371
|
+
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
1372
|
+
...s,
|
|
1373
|
+
runId: sessionRunMap.get(s.id) || s.runId || null,
|
|
1374
|
+
lines: s.lines.slice(-500)
|
|
1375
|
+
}));
|
|
1376
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(data));
|
|
1377
|
+
} catch {}
|
|
1378
|
+
}
|
|
1369
1379
|
|
|
1370
1380
|
function restoreSessions() {
|
|
1371
1381
|
try {
|
|
1372
|
-
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
1373
|
-
if (!raw) return;
|
|
1374
|
-
const saved = JSON.parse(raw);
|
|
1375
|
-
consoleSessions.push(...saved);
|
|
1376
|
-
for (const s of consoleSessions) {
|
|
1377
|
-
if (s.runId) {
|
|
1378
|
-
runSessionMap.set(s.runId, s.id);
|
|
1379
|
-
sessionRunMap.set(s.id, s.runId);
|
|
1380
|
-
}
|
|
1381
|
-
}
|
|
1382
|
-
if (consoleSessions.length > 0) {
|
|
1383
|
-
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
1384
|
-
renderTabs();
|
|
1385
|
-
renderActiveSession();
|
|
1382
|
+
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
1383
|
+
if (!raw) return;
|
|
1384
|
+
const saved = JSON.parse(raw);
|
|
1385
|
+
consoleSessions.push(...saved);
|
|
1386
|
+
for (const s of consoleSessions) {
|
|
1387
|
+
if (s.runId) {
|
|
1388
|
+
runSessionMap.set(s.runId, s.id);
|
|
1389
|
+
sessionRunMap.set(s.id, s.runId);
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if (consoleSessions.length > 0) {
|
|
1393
|
+
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
1394
|
+
renderTabs();
|
|
1395
|
+
renderActiveSession();
|
|
1386
1396
|
}
|
|
1387
1397
|
} catch {}
|
|
1388
1398
|
}
|
|
1389
1399
|
|
|
1390
|
-
function createSession(title, status = 'running', runId = null) {
|
|
1391
|
-
if (consoleSessions.length >= SESSION_MAX) {
|
|
1392
|
-
const dropped = consoleSessions.shift();
|
|
1393
|
-
if (dropped?.id) {
|
|
1394
|
-
const droppedRunId = sessionRunMap.get(dropped.id);
|
|
1395
|
-
if (droppedRunId) runSessionMap.delete(droppedRunId);
|
|
1396
|
-
sessionRunMap.delete(dropped.id);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
const s = { id: _sessionId(), title, lines: [], status, startTime: Date.now(), runId };
|
|
1400
|
-
consoleSessions.push(s);
|
|
1401
|
-
activeSessionId = s.id;
|
|
1402
|
-
if (runId) {
|
|
1403
|
-
runSessionMap.set(runId, s.id);
|
|
1404
|
-
sessionRunMap.set(s.id, runId);
|
|
1405
|
-
}
|
|
1406
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
1407
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
1408
|
-
renderTabs();
|
|
1409
|
-
renderActiveSession();
|
|
1410
|
-
saveSessions();
|
|
1400
|
+
function createSession(title, status = 'running', runId = null) {
|
|
1401
|
+
if (consoleSessions.length >= SESSION_MAX) {
|
|
1402
|
+
const dropped = consoleSessions.shift();
|
|
1403
|
+
if (dropped?.id) {
|
|
1404
|
+
const droppedRunId = sessionRunMap.get(dropped.id);
|
|
1405
|
+
if (droppedRunId) runSessionMap.delete(droppedRunId);
|
|
1406
|
+
sessionRunMap.delete(dropped.id);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
const s = { id: _sessionId(), title, lines: [], status, startTime: Date.now(), runId };
|
|
1410
|
+
consoleSessions.push(s);
|
|
1411
|
+
activeSessionId = s.id;
|
|
1412
|
+
if (runId) {
|
|
1413
|
+
runSessionMap.set(runId, s.id);
|
|
1414
|
+
sessionRunMap.set(s.id, runId);
|
|
1415
|
+
}
|
|
1416
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
1417
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
1418
|
+
renderTabs();
|
|
1419
|
+
renderActiveSession();
|
|
1420
|
+
saveSessions();
|
|
1411
1421
|
return s.id;
|
|
1412
1422
|
}
|
|
1413
1423
|
|
|
1414
|
-
function switchSession(id) {
|
|
1415
|
-
activeSessionId = id;
|
|
1416
|
-
currentRunId = sessionRunMap.get(id) || null;
|
|
1417
|
-
const s = consoleSessions.find(s => s.id === id);
|
|
1418
|
-
if (s) {
|
|
1419
|
-
const statusText = s.status === 'running' ? 'работает…'
|
|
1420
|
-
: s.status === 'ok' ? '✅ готово'
|
|
1421
|
-
: s.status === 'error' ? '❌ ошибка'
|
|
1424
|
+
function switchSession(id) {
|
|
1425
|
+
activeSessionId = id;
|
|
1426
|
+
currentRunId = sessionRunMap.get(id) || null;
|
|
1427
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
1428
|
+
if (s) {
|
|
1429
|
+
const statusText = s.status === 'running' ? 'работает…'
|
|
1430
|
+
: s.status === 'ok' ? '✅ готово'
|
|
1431
|
+
: s.status === 'error' ? '❌ ошибка'
|
|
1422
1432
|
: '';
|
|
1423
1433
|
document.getElementById('agentPanelTitle').textContent = s.title;
|
|
1424
1434
|
document.getElementById('agentPanelStatus').textContent = statusText;
|
|
1425
|
-
}
|
|
1426
|
-
renderTabs();
|
|
1427
|
-
renderActiveSession();
|
|
1428
|
-
}
|
|
1429
|
-
|
|
1430
|
-
function closeSession(id) {
|
|
1431
|
-
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
1432
|
-
if (idx === -1) return;
|
|
1433
|
-
const runId = sessionRunMap.get(id);
|
|
1434
|
-
if (runId) {
|
|
1435
|
-
sessionRunMap.delete(id);
|
|
1436
|
-
runSessionMap.delete(runId);
|
|
1437
|
-
}
|
|
1438
|
-
consoleSessions.splice(idx, 1);
|
|
1439
|
-
if (activeSessionId === id) {
|
|
1440
|
-
activeSessionId = consoleSessions.length > 0
|
|
1441
|
-
? consoleSessions[Math.min(idx, consoleSessions.length - 1)].id
|
|
1442
|
-
: null;
|
|
1443
|
-
}
|
|
1444
|
-
renderTabs();
|
|
1445
|
-
renderActiveSession();
|
|
1446
|
-
saveSessions();
|
|
1447
|
-
}
|
|
1448
|
-
|
|
1449
|
-
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
1450
|
-
const s = consoleSessions.find(s => s.id === id);
|
|
1451
|
-
if (!s) return;
|
|
1435
|
+
}
|
|
1436
|
+
renderTabs();
|
|
1437
|
+
renderActiveSession();
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
function closeSession(id) {
|
|
1441
|
+
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
1442
|
+
if (idx === -1) return;
|
|
1443
|
+
const runId = sessionRunMap.get(id);
|
|
1444
|
+
if (runId) {
|
|
1445
|
+
sessionRunMap.delete(id);
|
|
1446
|
+
runSessionMap.delete(runId);
|
|
1447
|
+
}
|
|
1448
|
+
consoleSessions.splice(idx, 1);
|
|
1449
|
+
if (activeSessionId === id) {
|
|
1450
|
+
activeSessionId = consoleSessions.length > 0
|
|
1451
|
+
? consoleSessions[Math.min(idx, consoleSessions.length - 1)].id
|
|
1452
|
+
: null;
|
|
1453
|
+
}
|
|
1454
|
+
renderTabs();
|
|
1455
|
+
renderActiveSession();
|
|
1456
|
+
saveSessions();
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
1460
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
1461
|
+
if (!s) return;
|
|
1452
1462
|
let stored;
|
|
1453
1463
|
if (typeof lineOrNode === 'string') {
|
|
1454
1464
|
stored = { text: lineOrNode, isError, isDim };
|
|
1455
1465
|
} else {
|
|
1456
1466
|
stored = { html: lineOrNode.outerHTML };
|
|
1457
|
-
}
|
|
1458
|
-
s.lines.push(stored);
|
|
1459
|
-
if (s.lines.length > SESSION_LINE_LIMIT) {
|
|
1460
|
-
s.lines.splice(0, s.lines.length - SESSION_LINE_LIMIT);
|
|
1461
|
-
}
|
|
1462
|
-
if (activeSessionId === id) {
|
|
1463
|
-
renderActiveSession();
|
|
1464
|
-
}
|
|
1465
|
-
}
|
|
1467
|
+
}
|
|
1468
|
+
s.lines.push(stored);
|
|
1469
|
+
if (s.lines.length > SESSION_LINE_LIMIT) {
|
|
1470
|
+
s.lines.splice(0, s.lines.length - SESSION_LINE_LIMIT);
|
|
1471
|
+
}
|
|
1472
|
+
if (activeSessionId === id) {
|
|
1473
|
+
renderActiveSession();
|
|
1474
|
+
}
|
|
1475
|
+
}
|
|
1466
1476
|
|
|
1467
1477
|
function updateSessionStatus(id, status) {
|
|
1468
1478
|
const s = consoleSessions.find(s => s.id === id);
|
|
@@ -1490,9 +1500,9 @@ function renderTabs() {
|
|
|
1490
1500
|
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1491
1501
|
}
|
|
1492
1502
|
|
|
1493
|
-
function copyTerminalContent() {
|
|
1494
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1495
|
-
if (!s || s.lines.length === 0) return;
|
|
1503
|
+
function copyTerminalContent() {
|
|
1504
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1505
|
+
if (!s || s.lines.length === 0) return;
|
|
1496
1506
|
// Extract plain text from each line (strip HTML for rich nodes)
|
|
1497
1507
|
const text = s.lines.map(l => {
|
|
1498
1508
|
if (l.text !== undefined) return l.text;
|
|
@@ -1510,214 +1520,214 @@ function copyTerminalContent() {
|
|
|
1510
1520
|
btn.textContent = '✓';
|
|
1511
1521
|
btn.classList.add('copied');
|
|
1512
1522
|
setTimeout(() => { btn.textContent = prev; btn.classList.remove('copied'); }, 1500);
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
|
|
1516
|
-
function downloadTextArtifact(filename, content) {
|
|
1517
|
-
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
|
1518
|
-
const a = document.createElement('a');
|
|
1519
|
-
a.href = URL.createObjectURL(blob);
|
|
1520
|
-
a.download = filename;
|
|
1521
|
-
document.body.appendChild(a);
|
|
1522
|
-
a.click();
|
|
1523
|
-
setTimeout(() => {
|
|
1524
|
-
URL.revokeObjectURL(a.href);
|
|
1525
|
-
a.remove();
|
|
1526
|
-
}, 100);
|
|
1527
|
-
}
|
|
1528
|
-
|
|
1529
|
-
function getActiveRunForExport() {
|
|
1530
|
-
const runId = sessionRunMap.get(activeSessionId) || currentRunId;
|
|
1531
|
-
if (runId) {
|
|
1532
|
-
const fromState = (agentRunsState || []).find((r) => r.runId === runId);
|
|
1533
|
-
if (fromState) return fromState;
|
|
1534
|
-
}
|
|
1535
|
-
return null;
|
|
1536
|
-
}
|
|
1537
|
-
|
|
1538
|
-
function exportActiveRun() {
|
|
1539
|
-
const run = getActiveRunForExport();
|
|
1540
|
-
const session = consoleSessions.find((s) => s.id === activeSessionId);
|
|
1541
|
-
if (!run && !session) {
|
|
1542
|
-
flashToolbarHint('Нечего экспортировать');
|
|
1543
|
-
return;
|
|
1544
|
-
}
|
|
1545
|
-
const mode = window.prompt('Формат экспорта: md или json', 'md');
|
|
1546
|
-
const format = (mode || 'md').trim().toLowerCase() === 'json' ? 'json' : 'md';
|
|
1547
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1548
|
-
const runId = run?.runId || sessionRunMap.get(activeSessionId) || 'session';
|
|
1549
|
-
if (format === 'json') {
|
|
1550
|
-
const payload = {
|
|
1551
|
-
run,
|
|
1552
|
-
sessionTitle: session?.title || null,
|
|
1553
|
-
lines: (session?.lines || []).map((line) => extractLineText(line)),
|
|
1554
|
-
exportedAt: new Date().toISOString(),
|
|
1555
|
-
};
|
|
1556
|
-
downloadTextArtifact(`viberadar-run-${runId}-${ts}.json`, JSON.stringify(payload, null, 2));
|
|
1557
|
-
return;
|
|
1558
|
-
}
|
|
1559
|
-
const lines = (session?.lines || []).map((line) => extractLineText(line)).filter(Boolean);
|
|
1560
|
-
const outcomes = Array.isArray(run?.fileOutcomes) ? run.fileOutcomes : [];
|
|
1561
|
-
const stats = run?.validationStats || {};
|
|
1562
|
-
const md = [
|
|
1563
|
-
`# VibeRadar Run Export`,
|
|
1564
|
-
``,
|
|
1565
|
-
`- runId: ${run?.runId || runId}`,
|
|
1566
|
-
`- title: ${run?.title || session?.title || '-'}`,
|
|
1567
|
-
`- phase: ${run?.phase || '-'}`,
|
|
1568
|
-
`- exportedAt: ${new Date().toISOString()}`,
|
|
1569
|
-
``,
|
|
1570
|
-
`## Validation`,
|
|
1571
|
-
``,
|
|
1572
|
-
`- covered: ${stats.covered ?? 0}`,
|
|
1573
|
-
`- not-covered: ${stats.notCovered ?? 0}`,
|
|
1574
|
-
`- blocked: ${stats.blocked ?? 0}`,
|
|
1575
|
-
`- infra: ${stats.infra ?? 0}`,
|
|
1576
|
-
``,
|
|
1577
|
-
`## File Outcomes`,
|
|
1578
|
-
``,
|
|
1579
|
-
...outcomes.map((o) => `- ${o.status}: ${o.sourcePath}${o.testFile ? ` -> ${o.testFile}` : ''}${o.reason ? ` (${o.reason})` : ''}`),
|
|
1580
|
-
``,
|
|
1581
|
-
`## Logs`,
|
|
1582
|
-
``,
|
|
1583
|
-
'```text',
|
|
1584
|
-
...lines,
|
|
1585
|
-
'```',
|
|
1586
|
-
].join('\n');
|
|
1587
|
-
downloadTextArtifact(`viberadar-run-${runId}-${ts}.md`, md);
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
function renderActiveSession() {
|
|
1591
|
-
const term = document.getElementById('agentTerminal');
|
|
1592
|
-
if (!term) return;
|
|
1593
|
-
term.innerHTML = '';
|
|
1594
|
-
terminalMatchRefs = [];
|
|
1595
|
-
terminalCommandRefs = [];
|
|
1596
|
-
terminalErrorRefs = [];
|
|
1597
|
-
terminalMatchCursor = -1;
|
|
1598
|
-
terminalCommandCursor = -1;
|
|
1599
|
-
terminalErrorCursor = -1;
|
|
1600
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1601
|
-
if (!s) {
|
|
1602
|
-
document.getElementById('agentSearchMeta').textContent = '0 matches';
|
|
1603
|
-
updateToolbarButtonsState();
|
|
1604
|
-
return;
|
|
1605
|
-
}
|
|
1606
|
-
const normalizedQuery = (terminalSearchQuery || '').trim();
|
|
1607
|
-
let regex = null;
|
|
1608
|
-
if (normalizedQuery && terminalSearchRegex) {
|
|
1609
|
-
try { regex = new RegExp(normalizedQuery, 'i'); } catch { regex = null; }
|
|
1610
|
-
}
|
|
1611
|
-
for (let i = 0; i < s.lines.length; i++) {
|
|
1612
|
-
const ln = s.lines[i];
|
|
1613
|
-
const text = extractLineText(ln);
|
|
1614
|
-
const isErrorLine = !!ln.isError || /(^|\s)❌/.test(text) || /\berror\b/i.test(text);
|
|
1615
|
-
const isCommandLine = /^\s*⚡\s*\$/.test(text);
|
|
1616
|
-
if (terminalSearchCurrentRunOnly && currentRunId && s.runId && s.runId !== currentRunId) continue;
|
|
1617
|
-
if (terminalSearchErrorsOnly && !isErrorLine) continue;
|
|
1618
|
-
let isMatch = false;
|
|
1619
|
-
if (!normalizedQuery) {
|
|
1620
|
-
isMatch = false;
|
|
1621
|
-
} else if (regex) {
|
|
1622
|
-
isMatch = regex.test(text);
|
|
1623
|
-
} else {
|
|
1624
|
-
isMatch = text.toLowerCase().includes(normalizedQuery.toLowerCase());
|
|
1625
|
-
}
|
|
1626
|
-
if (normalizedQuery && !isMatch) continue;
|
|
1627
|
-
|
|
1628
|
-
const el = document.createElement('div');
|
|
1629
|
-
if (ln.html) {
|
|
1630
|
-
el.innerHTML = ln.html;
|
|
1631
|
-
} else {
|
|
1632
|
-
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1633
|
-
el.textContent = ln.text;
|
|
1634
|
-
}
|
|
1635
|
-
if (isCommandLine) el.classList.add('command');
|
|
1636
|
-
if (isMatch) el.classList.add('match');
|
|
1637
|
-
el.dataset.lineIndex = String(i);
|
|
1638
|
-
term.appendChild(el);
|
|
1639
|
-
if (isMatch) terminalMatchRefs.push(el);
|
|
1640
|
-
if (isCommandLine) terminalCommandRefs.push(el);
|
|
1641
|
-
if (isErrorLine) terminalErrorRefs.push(el);
|
|
1642
|
-
}
|
|
1643
|
-
term.scrollTop = term.scrollHeight;
|
|
1644
|
-
document.getElementById('agentSearchMeta').textContent =
|
|
1645
|
-
`${terminalMatchRefs.length} matches • ${terminalCommandRefs.length} commands • ${terminalErrorRefs.length} errors`;
|
|
1646
|
-
updateToolbarButtonsState();
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
function updateToolbarButtonsState() {
|
|
1650
|
-
const setState = (id, enabled) => {
|
|
1651
|
-
const el = document.getElementById(id);
|
|
1652
|
-
if (!el) return;
|
|
1653
|
-
el.disabled = !enabled;
|
|
1654
|
-
};
|
|
1655
|
-
setState('btnMatchPrev', terminalMatchRefs.length > 0);
|
|
1656
|
-
setState('btnMatchNext', terminalMatchRefs.length > 0);
|
|
1657
|
-
setState('btnCmdPrev', terminalCommandRefs.length > 0);
|
|
1658
|
-
setState('btnCmdNext', terminalCommandRefs.length > 0);
|
|
1659
|
-
setState('btnErrPrev', terminalErrorRefs.length > 0);
|
|
1660
|
-
setState('btnErrNext', terminalErrorRefs.length > 0);
|
|
1661
|
-
const hasExport = !!getActiveRunForExport() || !!consoleSessions.find((s) => s.id === activeSessionId);
|
|
1662
|
-
setState('btnExportRun', hasExport);
|
|
1663
|
-
}
|
|
1664
|
-
|
|
1665
|
-
function flashToolbarHint(text) {
|
|
1666
|
-
const meta = document.getElementById('agentSearchMeta');
|
|
1667
|
-
if (!meta) return;
|
|
1668
|
-
const previous = meta.textContent;
|
|
1669
|
-
meta.textContent = text;
|
|
1670
|
-
meta.style.color = 'var(--yellow)';
|
|
1671
|
-
setTimeout(() => {
|
|
1672
|
-
meta.style.color = '';
|
|
1673
|
-
if (meta.textContent === text) {
|
|
1674
|
-
meta.textContent = previous || '0 matches';
|
|
1675
|
-
}
|
|
1676
|
-
}, 1400);
|
|
1677
|
-
}
|
|
1678
|
-
|
|
1679
|
-
function extractLineText(line) {
|
|
1680
|
-
if (!line) return '';
|
|
1681
|
-
if (line.text !== undefined) return String(line.text || '');
|
|
1682
|
-
if (line.html) {
|
|
1683
|
-
const tmp = document.createElement('div');
|
|
1684
|
-
tmp.innerHTML = line.html;
|
|
1685
|
-
return tmp.innerText || '';
|
|
1686
|
-
}
|
|
1687
|
-
return '';
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
|
-
function jumpByRefs(refs, dir, cursorName) {
|
|
1691
|
-
if (!refs.length) {
|
|
1692
|
-
const label = cursorName === 'match' ? 'совпадений' : cursorName === 'command' ? 'команд' : 'ошибок';
|
|
1693
|
-
flashToolbarHint(`Нет ${label} для навигации`);
|
|
1694
|
-
return;
|
|
1695
|
-
}
|
|
1696
|
-
if (cursorName === 'match') {
|
|
1697
|
-
terminalMatchCursor = (terminalMatchCursor + dir + refs.length) % refs.length;
|
|
1698
|
-
refs[terminalMatchCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1699
|
-
return;
|
|
1700
|
-
}
|
|
1701
|
-
if (cursorName === 'command') {
|
|
1702
|
-
terminalCommandCursor = (terminalCommandCursor + dir + refs.length) % refs.length;
|
|
1703
|
-
refs[terminalCommandCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1704
|
-
return;
|
|
1705
|
-
}
|
|
1706
|
-
terminalErrorCursor = (terminalErrorCursor + dir + refs.length) % refs.length;
|
|
1707
|
-
refs[terminalErrorCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1708
|
-
}
|
|
1709
|
-
|
|
1710
|
-
function jumpTerminalMatch(dir) {
|
|
1711
|
-
jumpByRefs(terminalMatchRefs, dir > 0 ? 1 : -1, 'match');
|
|
1712
|
-
}
|
|
1713
|
-
|
|
1714
|
-
function jumpCommand(dir) {
|
|
1715
|
-
jumpByRefs(terminalCommandRefs, dir > 0 ? 1 : -1, 'command');
|
|
1716
|
-
}
|
|
1717
|
-
|
|
1718
|
-
function jumpError(dir) {
|
|
1719
|
-
jumpByRefs(terminalErrorRefs, dir > 0 ? 1 : -1, 'error');
|
|
1720
|
-
}
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
function downloadTextArtifact(filename, content) {
|
|
1527
|
+
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
|
1528
|
+
const a = document.createElement('a');
|
|
1529
|
+
a.href = URL.createObjectURL(blob);
|
|
1530
|
+
a.download = filename;
|
|
1531
|
+
document.body.appendChild(a);
|
|
1532
|
+
a.click();
|
|
1533
|
+
setTimeout(() => {
|
|
1534
|
+
URL.revokeObjectURL(a.href);
|
|
1535
|
+
a.remove();
|
|
1536
|
+
}, 100);
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function getActiveRunForExport() {
|
|
1540
|
+
const runId = sessionRunMap.get(activeSessionId) || currentRunId;
|
|
1541
|
+
if (runId) {
|
|
1542
|
+
const fromState = (agentRunsState || []).find((r) => r.runId === runId);
|
|
1543
|
+
if (fromState) return fromState;
|
|
1544
|
+
}
|
|
1545
|
+
return null;
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
function exportActiveRun() {
|
|
1549
|
+
const run = getActiveRunForExport();
|
|
1550
|
+
const session = consoleSessions.find((s) => s.id === activeSessionId);
|
|
1551
|
+
if (!run && !session) {
|
|
1552
|
+
flashToolbarHint('Нечего экспортировать');
|
|
1553
|
+
return;
|
|
1554
|
+
}
|
|
1555
|
+
const mode = window.prompt('Формат экспорта: md или json', 'md');
|
|
1556
|
+
const format = (mode || 'md').trim().toLowerCase() === 'json' ? 'json' : 'md';
|
|
1557
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1558
|
+
const runId = run?.runId || sessionRunMap.get(activeSessionId) || 'session';
|
|
1559
|
+
if (format === 'json') {
|
|
1560
|
+
const payload = {
|
|
1561
|
+
run,
|
|
1562
|
+
sessionTitle: session?.title || null,
|
|
1563
|
+
lines: (session?.lines || []).map((line) => extractLineText(line)),
|
|
1564
|
+
exportedAt: new Date().toISOString(),
|
|
1565
|
+
};
|
|
1566
|
+
downloadTextArtifact(`viberadar-run-${runId}-${ts}.json`, JSON.stringify(payload, null, 2));
|
|
1567
|
+
return;
|
|
1568
|
+
}
|
|
1569
|
+
const lines = (session?.lines || []).map((line) => extractLineText(line)).filter(Boolean);
|
|
1570
|
+
const outcomes = Array.isArray(run?.fileOutcomes) ? run.fileOutcomes : [];
|
|
1571
|
+
const stats = run?.validationStats || {};
|
|
1572
|
+
const md = [
|
|
1573
|
+
`# VibeRadar Run Export`,
|
|
1574
|
+
``,
|
|
1575
|
+
`- runId: ${run?.runId || runId}`,
|
|
1576
|
+
`- title: ${run?.title || session?.title || '-'}`,
|
|
1577
|
+
`- phase: ${run?.phase || '-'}`,
|
|
1578
|
+
`- exportedAt: ${new Date().toISOString()}`,
|
|
1579
|
+
``,
|
|
1580
|
+
`## Validation`,
|
|
1581
|
+
``,
|
|
1582
|
+
`- covered: ${stats.covered ?? 0}`,
|
|
1583
|
+
`- not-covered: ${stats.notCovered ?? 0}`,
|
|
1584
|
+
`- blocked: ${stats.blocked ?? 0}`,
|
|
1585
|
+
`- infra: ${stats.infra ?? 0}`,
|
|
1586
|
+
``,
|
|
1587
|
+
`## File Outcomes`,
|
|
1588
|
+
``,
|
|
1589
|
+
...outcomes.map((o) => `- ${o.status}: ${o.sourcePath}${o.testFile ? ` -> ${o.testFile}` : ''}${o.reason ? ` (${o.reason})` : ''}`),
|
|
1590
|
+
``,
|
|
1591
|
+
`## Logs`,
|
|
1592
|
+
``,
|
|
1593
|
+
'```text',
|
|
1594
|
+
...lines,
|
|
1595
|
+
'```',
|
|
1596
|
+
].join('\n');
|
|
1597
|
+
downloadTextArtifact(`viberadar-run-${runId}-${ts}.md`, md);
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
function renderActiveSession() {
|
|
1601
|
+
const term = document.getElementById('agentTerminal');
|
|
1602
|
+
if (!term) return;
|
|
1603
|
+
term.innerHTML = '';
|
|
1604
|
+
terminalMatchRefs = [];
|
|
1605
|
+
terminalCommandRefs = [];
|
|
1606
|
+
terminalErrorRefs = [];
|
|
1607
|
+
terminalMatchCursor = -1;
|
|
1608
|
+
terminalCommandCursor = -1;
|
|
1609
|
+
terminalErrorCursor = -1;
|
|
1610
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1611
|
+
if (!s) {
|
|
1612
|
+
document.getElementById('agentSearchMeta').textContent = '0 matches';
|
|
1613
|
+
updateToolbarButtonsState();
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const normalizedQuery = (terminalSearchQuery || '').trim();
|
|
1617
|
+
let regex = null;
|
|
1618
|
+
if (normalizedQuery && terminalSearchRegex) {
|
|
1619
|
+
try { regex = new RegExp(normalizedQuery, 'i'); } catch { regex = null; }
|
|
1620
|
+
}
|
|
1621
|
+
for (let i = 0; i < s.lines.length; i++) {
|
|
1622
|
+
const ln = s.lines[i];
|
|
1623
|
+
const text = extractLineText(ln);
|
|
1624
|
+
const isErrorLine = !!ln.isError || /(^|\s)❌/.test(text) || /\berror\b/i.test(text);
|
|
1625
|
+
const isCommandLine = /^\s*⚡\s*\$/.test(text);
|
|
1626
|
+
if (terminalSearchCurrentRunOnly && currentRunId && s.runId && s.runId !== currentRunId) continue;
|
|
1627
|
+
if (terminalSearchErrorsOnly && !isErrorLine) continue;
|
|
1628
|
+
let isMatch = false;
|
|
1629
|
+
if (!normalizedQuery) {
|
|
1630
|
+
isMatch = false;
|
|
1631
|
+
} else if (regex) {
|
|
1632
|
+
isMatch = regex.test(text);
|
|
1633
|
+
} else {
|
|
1634
|
+
isMatch = text.toLowerCase().includes(normalizedQuery.toLowerCase());
|
|
1635
|
+
}
|
|
1636
|
+
if (normalizedQuery && !isMatch) continue;
|
|
1637
|
+
|
|
1638
|
+
const el = document.createElement('div');
|
|
1639
|
+
if (ln.html) {
|
|
1640
|
+
el.innerHTML = ln.html;
|
|
1641
|
+
} else {
|
|
1642
|
+
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1643
|
+
el.textContent = ln.text;
|
|
1644
|
+
}
|
|
1645
|
+
if (isCommandLine) el.classList.add('command');
|
|
1646
|
+
if (isMatch) el.classList.add('match');
|
|
1647
|
+
el.dataset.lineIndex = String(i);
|
|
1648
|
+
term.appendChild(el);
|
|
1649
|
+
if (isMatch) terminalMatchRefs.push(el);
|
|
1650
|
+
if (isCommandLine) terminalCommandRefs.push(el);
|
|
1651
|
+
if (isErrorLine) terminalErrorRefs.push(el);
|
|
1652
|
+
}
|
|
1653
|
+
term.scrollTop = term.scrollHeight;
|
|
1654
|
+
document.getElementById('agentSearchMeta').textContent =
|
|
1655
|
+
`${terminalMatchRefs.length} matches • ${terminalCommandRefs.length} commands • ${terminalErrorRefs.length} errors`;
|
|
1656
|
+
updateToolbarButtonsState();
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
function updateToolbarButtonsState() {
|
|
1660
|
+
const setState = (id, enabled) => {
|
|
1661
|
+
const el = document.getElementById(id);
|
|
1662
|
+
if (!el) return;
|
|
1663
|
+
el.disabled = !enabled;
|
|
1664
|
+
};
|
|
1665
|
+
setState('btnMatchPrev', terminalMatchRefs.length > 0);
|
|
1666
|
+
setState('btnMatchNext', terminalMatchRefs.length > 0);
|
|
1667
|
+
setState('btnCmdPrev', terminalCommandRefs.length > 0);
|
|
1668
|
+
setState('btnCmdNext', terminalCommandRefs.length > 0);
|
|
1669
|
+
setState('btnErrPrev', terminalErrorRefs.length > 0);
|
|
1670
|
+
setState('btnErrNext', terminalErrorRefs.length > 0);
|
|
1671
|
+
const hasExport = !!getActiveRunForExport() || !!consoleSessions.find((s) => s.id === activeSessionId);
|
|
1672
|
+
setState('btnExportRun', hasExport);
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
function flashToolbarHint(text) {
|
|
1676
|
+
const meta = document.getElementById('agentSearchMeta');
|
|
1677
|
+
if (!meta) return;
|
|
1678
|
+
const previous = meta.textContent;
|
|
1679
|
+
meta.textContent = text;
|
|
1680
|
+
meta.style.color = 'var(--yellow)';
|
|
1681
|
+
setTimeout(() => {
|
|
1682
|
+
meta.style.color = '';
|
|
1683
|
+
if (meta.textContent === text) {
|
|
1684
|
+
meta.textContent = previous || '0 matches';
|
|
1685
|
+
}
|
|
1686
|
+
}, 1400);
|
|
1687
|
+
}
|
|
1688
|
+
|
|
1689
|
+
function extractLineText(line) {
|
|
1690
|
+
if (!line) return '';
|
|
1691
|
+
if (line.text !== undefined) return String(line.text || '');
|
|
1692
|
+
if (line.html) {
|
|
1693
|
+
const tmp = document.createElement('div');
|
|
1694
|
+
tmp.innerHTML = line.html;
|
|
1695
|
+
return tmp.innerText || '';
|
|
1696
|
+
}
|
|
1697
|
+
return '';
|
|
1698
|
+
}
|
|
1699
|
+
|
|
1700
|
+
function jumpByRefs(refs, dir, cursorName) {
|
|
1701
|
+
if (!refs.length) {
|
|
1702
|
+
const label = cursorName === 'match' ? 'совпадений' : cursorName === 'command' ? 'команд' : 'ошибок';
|
|
1703
|
+
flashToolbarHint(`Нет ${label} для навигации`);
|
|
1704
|
+
return;
|
|
1705
|
+
}
|
|
1706
|
+
if (cursorName === 'match') {
|
|
1707
|
+
terminalMatchCursor = (terminalMatchCursor + dir + refs.length) % refs.length;
|
|
1708
|
+
refs[terminalMatchCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1709
|
+
return;
|
|
1710
|
+
}
|
|
1711
|
+
if (cursorName === 'command') {
|
|
1712
|
+
terminalCommandCursor = (terminalCommandCursor + dir + refs.length) % refs.length;
|
|
1713
|
+
refs[terminalCommandCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1714
|
+
return;
|
|
1715
|
+
}
|
|
1716
|
+
terminalErrorCursor = (terminalErrorCursor + dir + refs.length) % refs.length;
|
|
1717
|
+
refs[terminalErrorCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1718
|
+
}
|
|
1719
|
+
|
|
1720
|
+
function jumpTerminalMatch(dir) {
|
|
1721
|
+
jumpByRefs(terminalMatchRefs, dir > 0 ? 1 : -1, 'match');
|
|
1722
|
+
}
|
|
1723
|
+
|
|
1724
|
+
function jumpCommand(dir) {
|
|
1725
|
+
jumpByRefs(terminalCommandRefs, dir > 0 ? 1 : -1, 'command');
|
|
1726
|
+
}
|
|
1727
|
+
|
|
1728
|
+
function jumpError(dir) {
|
|
1729
|
+
jumpByRefs(terminalErrorRefs, dir > 0 ? 1 : -1, 'error');
|
|
1730
|
+
}
|
|
1721
1731
|
|
|
1722
1732
|
async function setAgent(agent) {
|
|
1723
1733
|
await fetch('/api/set-agent', {
|
|
@@ -1766,157 +1776,157 @@ function isFileAgentActive(relPath) {
|
|
|
1766
1776
|
return null;
|
|
1767
1777
|
}
|
|
1768
1778
|
|
|
1769
|
-
function updateQueueBadge(n) {
|
|
1770
|
-
document.getElementById('agentQueueCount').textContent = n;
|
|
1771
|
-
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1772
|
-
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1773
|
-
renderQueuePanel();
|
|
1774
|
-
}
|
|
1775
|
-
|
|
1776
|
-
function renderQueuePanel() {
|
|
1777
|
-
const panel = document.getElementById('agentQueuePanel');
|
|
1778
|
-
if (!panel) return;
|
|
1779
|
-
if (!Array.isArray(agentQueueState) || agentQueueState.length === 0) {
|
|
1780
|
-
panel.style.display = 'none';
|
|
1781
|
-
panel.innerHTML = '';
|
|
1782
|
-
return;
|
|
1783
|
-
}
|
|
1784
|
-
panel.style.display = 'block';
|
|
1785
|
-
panel.innerHTML = `
|
|
1786
|
-
<div class="agent-queue-title">Очередь задач (${agentQueueState.length})</div>
|
|
1787
|
-
${agentQueueState.map((item, idx) => `
|
|
1788
|
-
<div class="agent-queue-item">
|
|
1789
|
-
<span class="agent-queue-pos">#${item.position ?? idx + 1}</span>
|
|
1790
|
-
<span title="${escapeHtml(item.title || '')}">${escapeHtml(item.title || item.task || item.runId)}</span>
|
|
1791
|
-
<span style="color:var(--dim)">(${escapeHtml(item.task || 'task')})</span>
|
|
1792
|
-
<div class="agent-queue-actions">
|
|
1793
|
-
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','up')" title="Сдвинуть вверх">↑</button>
|
|
1794
|
-
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','down')" title="Сдвинуть вниз">↓</button>
|
|
1795
|
-
<button class="agent-queue-action" onclick="cancelQueueItem('${item.runId}')" title="Отменить задачу">✕</button>
|
|
1796
|
-
<button class="agent-queue-action" onclick="retryRun('${item.runId}')" title="Retry">↻</button>
|
|
1797
|
-
</div>
|
|
1798
|
-
</div>
|
|
1799
|
-
`).join('')}
|
|
1800
|
-
`;
|
|
1801
|
-
}
|
|
1802
|
-
|
|
1803
|
-
async function cancelQueueItem(runId) {
|
|
1804
|
-
await fetch(`/api/queue/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
|
|
1805
|
-
await loadAgentState();
|
|
1806
|
-
}
|
|
1807
|
-
|
|
1808
|
-
async function retryRun(runId) {
|
|
1809
|
-
await fetch(`/api/queue/${encodeURIComponent(runId)}/retry`, { method: 'POST' });
|
|
1810
|
-
await loadAgentState();
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
async function reorderQueueItem(runId, direction) {
|
|
1814
|
-
await fetch(`/api/queue/${encodeURIComponent(runId)}/reorder`, {
|
|
1815
|
-
method: 'POST',
|
|
1816
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1817
|
-
body: JSON.stringify({ direction }),
|
|
1818
|
-
});
|
|
1819
|
-
await loadAgentState();
|
|
1820
|
-
}
|
|
1821
|
-
|
|
1822
|
-
function ensureSessionForRun(run, makeActive = false) {
|
|
1823
|
-
if (!run?.runId) return null;
|
|
1824
|
-
let sessionId = runSessionMap.get(run.runId);
|
|
1825
|
-
if (!sessionId) {
|
|
1826
|
-
sessionId = createSession(run.title || `Run ${run.runId}`, run.phase === 'failed' ? 'error' : run.phase === 'completed' ? 'ok' : 'running', run.runId);
|
|
1827
|
-
const meta = [`runId: ${run.runId}`, `phase: ${run.phase}`];
|
|
1828
|
-
if (Array.isArray(run.targetSourcePaths) && run.targetSourcePaths.length > 0) {
|
|
1829
|
-
meta.push(`targets: ${run.targetSourcePaths.length}`);
|
|
1830
|
-
}
|
|
1831
|
-
appendToSession(sessionId, `ℹ ${meta.join(' • ')}`, false, true);
|
|
1832
|
-
}
|
|
1833
|
-
if (makeActive) switchSession(sessionId);
|
|
1834
|
-
return sessionId;
|
|
1835
|
-
}
|
|
1836
|
-
|
|
1837
|
-
function applyAgentStateSnapshot(state) {
|
|
1838
|
-
if (!state) return;
|
|
1839
|
-
agentQueueState = Array.isArray(state.queue) ? state.queue : [];
|
|
1840
|
-
agentRunsState = Array.isArray(state.runs) ? state.runs : [];
|
|
1841
|
-
agentActiveRun = state.activeRun || null;
|
|
1842
|
-
updateQueueBadge(agentQueueState.length);
|
|
1843
|
-
if (agentActiveRun?.runId) {
|
|
1844
|
-
currentRunId = agentActiveRun.runId;
|
|
1845
|
-
ensureSessionForRun(agentActiveRun);
|
|
1846
|
-
setAgentRunning(['starting', 'running', 'validating'].includes(agentActiveRun.phase));
|
|
1847
|
-
} else {
|
|
1848
|
-
setAgentRunning(false);
|
|
1849
|
-
}
|
|
1850
|
-
}
|
|
1851
|
-
|
|
1852
|
-
async function loadAgentState() {
|
|
1853
|
-
try {
|
|
1854
|
-
const res = await fetch('/api/agent/state');
|
|
1855
|
-
if (!res.ok) return;
|
|
1856
|
-
const state = await res.json();
|
|
1857
|
-
applyAgentStateSnapshot(state);
|
|
1858
|
-
} catch {}
|
|
1859
|
-
}
|
|
1860
|
-
|
|
1861
|
-
function upsertRunState(run) {
|
|
1862
|
-
if (!run?.runId) return;
|
|
1863
|
-
const idx = agentRunsState.findIndex((r) => r.runId === run.runId);
|
|
1864
|
-
if (idx >= 0) agentRunsState[idx] = { ...agentRunsState[idx], ...run };
|
|
1865
|
-
else agentRunsState.push(run);
|
|
1866
|
-
if (agentRunsState.length > 80) {
|
|
1867
|
-
agentRunsState = agentRunsState.slice(agentRunsState.length - 80);
|
|
1868
|
-
}
|
|
1869
|
-
}
|
|
1870
|
-
|
|
1871
|
-
function updateSessionFromRun(run) {
|
|
1872
|
-
if (!run?.runId) return;
|
|
1873
|
-
upsertRunState(run);
|
|
1874
|
-
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
1875
|
-
setAgentRunning(true);
|
|
1876
|
-
currentRunId = run.runId;
|
|
1877
|
-
const sid = ensureSessionForRun(run);
|
|
1878
|
-
if (sid) {
|
|
1879
|
-
runningSessionId = sid;
|
|
1880
|
-
if (activeSessionId === sid) {
|
|
1881
|
-
document.getElementById('agentPanelTitle').textContent = '🤖 ' + (run.title || 'Agent');
|
|
1882
|
-
document.getElementById('agentPanelStatus').textContent = run.phase === 'validating' ? 'проверяю…' : 'работает…';
|
|
1883
|
-
}
|
|
1884
|
-
}
|
|
1885
|
-
return;
|
|
1886
|
-
}
|
|
1887
|
-
const sid = runSessionMap.get(run.runId);
|
|
1888
|
-
if (sid) {
|
|
1889
|
-
const status = run.phase === 'completed' ? 'ok' : run.phase === 'canceled' ? 'error' : run.phase === 'failed' ? 'error' : 'info';
|
|
1890
|
-
updateSessionStatus(sid, status);
|
|
1891
|
-
if (runningSessionId === sid) runningSessionId = null;
|
|
1892
|
-
if (activeSessionId === sid) {
|
|
1893
|
-
document.getElementById('agentPanelStatus').textContent =
|
|
1894
|
-
run.phase === 'completed' ? '✅ готово' : run.phase === 'canceled' ? '⏹ отменено' : run.phase === 'failed' ? '❌ ошибка' : run.phase;
|
|
1895
|
-
}
|
|
1896
|
-
}
|
|
1897
|
-
if (run.phase === 'completed' || run.phase === 'failed' || run.phase === 'canceled') {
|
|
1898
|
-
if (agentActiveRun?.runId === run.runId) agentActiveRun = null;
|
|
1899
|
-
if (currentRunId === run.runId) currentRunId = null;
|
|
1900
|
-
setAgentRunning(false);
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
|
|
1904
|
-
function refreshPathActivityFromState() {
|
|
1905
|
-
agentQueuedPaths.clear();
|
|
1906
|
-
(agentQueueState || []).forEach((q) => {
|
|
1907
|
-
getTaskFilePaths(q.task, q.featureKey, q.filePath, q.selectedFilePaths).forEach((p) => agentQueuedPaths.add(p));
|
|
1908
|
-
});
|
|
1909
|
-
if (!agentActiveRun || !['starting', 'running', 'validating'].includes(agentActiveRun.phase)) {
|
|
1910
|
-
agentRunningPaths.clear();
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
|
|
1914
|
-
async function cancelAgent() {
|
|
1915
|
-
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1916
|
-
await loadAgentState();
|
|
1917
|
-
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1918
|
-
if (runningSessionId) {
|
|
1919
|
-
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1779
|
+
function updateQueueBadge(n) {
|
|
1780
|
+
document.getElementById('agentQueueCount').textContent = n;
|
|
1781
|
+
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1782
|
+
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1783
|
+
renderQueuePanel();
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
function renderQueuePanel() {
|
|
1787
|
+
const panel = document.getElementById('agentQueuePanel');
|
|
1788
|
+
if (!panel) return;
|
|
1789
|
+
if (!Array.isArray(agentQueueState) || agentQueueState.length === 0) {
|
|
1790
|
+
panel.style.display = 'none';
|
|
1791
|
+
panel.innerHTML = '';
|
|
1792
|
+
return;
|
|
1793
|
+
}
|
|
1794
|
+
panel.style.display = 'block';
|
|
1795
|
+
panel.innerHTML = `
|
|
1796
|
+
<div class="agent-queue-title">Очередь задач (${agentQueueState.length})</div>
|
|
1797
|
+
${agentQueueState.map((item, idx) => `
|
|
1798
|
+
<div class="agent-queue-item">
|
|
1799
|
+
<span class="agent-queue-pos">#${item.position ?? idx + 1}</span>
|
|
1800
|
+
<span title="${escapeHtml(item.title || '')}">${escapeHtml(item.title || item.task || item.runId)}</span>
|
|
1801
|
+
<span style="color:var(--dim)">(${escapeHtml(item.task || 'task')})</span>
|
|
1802
|
+
<div class="agent-queue-actions">
|
|
1803
|
+
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','up')" title="Сдвинуть вверх">↑</button>
|
|
1804
|
+
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','down')" title="Сдвинуть вниз">↓</button>
|
|
1805
|
+
<button class="agent-queue-action" onclick="cancelQueueItem('${item.runId}')" title="Отменить задачу">✕</button>
|
|
1806
|
+
<button class="agent-queue-action" onclick="retryRun('${item.runId}')" title="Retry">↻</button>
|
|
1807
|
+
</div>
|
|
1808
|
+
</div>
|
|
1809
|
+
`).join('')}
|
|
1810
|
+
`;
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
async function cancelQueueItem(runId) {
|
|
1814
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
|
|
1815
|
+
await loadAgentState();
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1818
|
+
async function retryRun(runId) {
|
|
1819
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/retry`, { method: 'POST' });
|
|
1820
|
+
await loadAgentState();
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
async function reorderQueueItem(runId, direction) {
|
|
1824
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/reorder`, {
|
|
1825
|
+
method: 'POST',
|
|
1826
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1827
|
+
body: JSON.stringify({ direction }),
|
|
1828
|
+
});
|
|
1829
|
+
await loadAgentState();
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function ensureSessionForRun(run, makeActive = false) {
|
|
1833
|
+
if (!run?.runId) return null;
|
|
1834
|
+
let sessionId = runSessionMap.get(run.runId);
|
|
1835
|
+
if (!sessionId) {
|
|
1836
|
+
sessionId = createSession(run.title || `Run ${run.runId}`, run.phase === 'failed' ? 'error' : run.phase === 'completed' ? 'ok' : 'running', run.runId);
|
|
1837
|
+
const meta = [`runId: ${run.runId}`, `phase: ${run.phase}`];
|
|
1838
|
+
if (Array.isArray(run.targetSourcePaths) && run.targetSourcePaths.length > 0) {
|
|
1839
|
+
meta.push(`targets: ${run.targetSourcePaths.length}`);
|
|
1840
|
+
}
|
|
1841
|
+
appendToSession(sessionId, `ℹ ${meta.join(' • ')}`, false, true);
|
|
1842
|
+
}
|
|
1843
|
+
if (makeActive) switchSession(sessionId);
|
|
1844
|
+
return sessionId;
|
|
1845
|
+
}
|
|
1846
|
+
|
|
1847
|
+
function applyAgentStateSnapshot(state) {
|
|
1848
|
+
if (!state) return;
|
|
1849
|
+
agentQueueState = Array.isArray(state.queue) ? state.queue : [];
|
|
1850
|
+
agentRunsState = Array.isArray(state.runs) ? state.runs : [];
|
|
1851
|
+
agentActiveRun = state.activeRun || null;
|
|
1852
|
+
updateQueueBadge(agentQueueState.length);
|
|
1853
|
+
if (agentActiveRun?.runId) {
|
|
1854
|
+
currentRunId = agentActiveRun.runId;
|
|
1855
|
+
ensureSessionForRun(agentActiveRun);
|
|
1856
|
+
setAgentRunning(['starting', 'running', 'validating'].includes(agentActiveRun.phase));
|
|
1857
|
+
} else {
|
|
1858
|
+
setAgentRunning(false);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
async function loadAgentState() {
|
|
1863
|
+
try {
|
|
1864
|
+
const res = await fetch('/api/agent/state');
|
|
1865
|
+
if (!res.ok) return;
|
|
1866
|
+
const state = await res.json();
|
|
1867
|
+
applyAgentStateSnapshot(state);
|
|
1868
|
+
} catch {}
|
|
1869
|
+
}
|
|
1870
|
+
|
|
1871
|
+
function upsertRunState(run) {
|
|
1872
|
+
if (!run?.runId) return;
|
|
1873
|
+
const idx = agentRunsState.findIndex((r) => r.runId === run.runId);
|
|
1874
|
+
if (idx >= 0) agentRunsState[idx] = { ...agentRunsState[idx], ...run };
|
|
1875
|
+
else agentRunsState.push(run);
|
|
1876
|
+
if (agentRunsState.length > 80) {
|
|
1877
|
+
agentRunsState = agentRunsState.slice(agentRunsState.length - 80);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
function updateSessionFromRun(run) {
|
|
1882
|
+
if (!run?.runId) return;
|
|
1883
|
+
upsertRunState(run);
|
|
1884
|
+
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
1885
|
+
setAgentRunning(true);
|
|
1886
|
+
currentRunId = run.runId;
|
|
1887
|
+
const sid = ensureSessionForRun(run);
|
|
1888
|
+
if (sid) {
|
|
1889
|
+
runningSessionId = sid;
|
|
1890
|
+
if (activeSessionId === sid) {
|
|
1891
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + (run.title || 'Agent');
|
|
1892
|
+
document.getElementById('agentPanelStatus').textContent = run.phase === 'validating' ? 'проверяю…' : 'работает…';
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
return;
|
|
1896
|
+
}
|
|
1897
|
+
const sid = runSessionMap.get(run.runId);
|
|
1898
|
+
if (sid) {
|
|
1899
|
+
const status = run.phase === 'completed' ? 'ok' : run.phase === 'canceled' ? 'error' : run.phase === 'failed' ? 'error' : 'info';
|
|
1900
|
+
updateSessionStatus(sid, status);
|
|
1901
|
+
if (runningSessionId === sid) runningSessionId = null;
|
|
1902
|
+
if (activeSessionId === sid) {
|
|
1903
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
1904
|
+
run.phase === 'completed' ? '✅ готово' : run.phase === 'canceled' ? '⏹ отменено' : run.phase === 'failed' ? '❌ ошибка' : run.phase;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
if (run.phase === 'completed' || run.phase === 'failed' || run.phase === 'canceled') {
|
|
1908
|
+
if (agentActiveRun?.runId === run.runId) agentActiveRun = null;
|
|
1909
|
+
if (currentRunId === run.runId) currentRunId = null;
|
|
1910
|
+
setAgentRunning(false);
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
|
|
1914
|
+
function refreshPathActivityFromState() {
|
|
1915
|
+
agentQueuedPaths.clear();
|
|
1916
|
+
(agentQueueState || []).forEach((q) => {
|
|
1917
|
+
getTaskFilePaths(q.task, q.featureKey, q.filePath, q.selectedFilePaths).forEach((p) => agentQueuedPaths.add(p));
|
|
1918
|
+
});
|
|
1919
|
+
if (!agentActiveRun || !['starting', 'running', 'validating'].includes(agentActiveRun.phase)) {
|
|
1920
|
+
agentRunningPaths.clear();
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
|
|
1924
|
+
async function cancelAgent() {
|
|
1925
|
+
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1926
|
+
await loadAgentState();
|
|
1927
|
+
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1928
|
+
if (runningSessionId) {
|
|
1929
|
+
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1920
1930
|
updateSessionStatus(runningSessionId, 'error');
|
|
1921
1931
|
runningSessionId = null;
|
|
1922
1932
|
} else {
|
|
@@ -1924,11 +1934,11 @@ async function cancelAgent() {
|
|
|
1924
1934
|
}
|
|
1925
1935
|
}
|
|
1926
1936
|
|
|
1927
|
-
async function clearAgentQueue() {
|
|
1928
|
-
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1929
|
-
await loadAgentState();
|
|
1930
|
-
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1931
|
-
}
|
|
1937
|
+
async function clearAgentQueue() {
|
|
1938
|
+
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1939
|
+
await loadAgentState();
|
|
1940
|
+
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1941
|
+
}
|
|
1932
1942
|
|
|
1933
1943
|
// ─── File row more menu ──────────────────────────────────────────────────────
|
|
1934
1944
|
let _openFileMenu = null;
|
|
@@ -2060,7 +2070,7 @@ async function reauthAgent() {
|
|
|
2060
2070
|
await fetch('/api/agent-reauth', { method: 'POST' });
|
|
2061
2071
|
}
|
|
2062
2072
|
|
|
2063
|
-
async function runAgentTask(task, featureKey, filePath, selectedFilePaths) {
|
|
2073
|
+
async function runAgentTask(task, featureKey, filePath, selectedFilePaths, meta) {
|
|
2064
2074
|
document.getElementById('agentPanel').classList.add('open');
|
|
2065
2075
|
document.getElementById('termBtn').classList.add('term-active');
|
|
2066
2076
|
if (!agentRunning) {
|
|
@@ -2074,6 +2084,7 @@ async function runAgentTask(task, featureKey, filePath, selectedFilePaths) {
|
|
|
2074
2084
|
featureKey,
|
|
2075
2085
|
filePath: filePath || undefined,
|
|
2076
2086
|
selectedFilePaths: Array.isArray(selectedFilePaths) ? selectedFilePaths : undefined,
|
|
2087
|
+
meta: meta || undefined,
|
|
2077
2088
|
}),
|
|
2078
2089
|
});
|
|
2079
2090
|
}
|
|
@@ -2195,19 +2206,29 @@ function renderStats() {
|
|
|
2195
2206
|
|
|
2196
2207
|
let items;
|
|
2197
2208
|
if (contextMode === 'observability') {
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2209
|
+
const o = D.observability;
|
|
2210
|
+
if (o) {
|
|
2211
|
+
const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
|
|
2212
|
+
const structPct = Math.round(o.metrics.structured_completeness * 100);
|
|
2213
|
+
const missingCoverage = o.missingCriticalLogs.length;
|
|
2214
|
+
const noisyCount = o.topNoisyPatterns.length;
|
|
2215
|
+
items = [
|
|
2216
|
+
{ v: o.catalog.length, l: 'Источники логов' },
|
|
2217
|
+
{ v: noiseRatio + '%', l: 'Коэффициент шума', c: noiseRatio > 30 ? '#f85149' : noiseRatio > 10 ? '#e3b341' : undefined },
|
|
2218
|
+
{ v: structPct + '%', l: 'Структурированность', c: structPct < 50 ? '#f85149' : structPct < 80 ? '#e3b341' : undefined },
|
|
2219
|
+
{ v: missingCoverage, l: 'Нет покрытия', c: missingCoverage ? '#e3b341' : undefined },
|
|
2220
|
+
{ v: noisyCount, l: 'Шумных паттернов', c: noisyCount ? '#e3b341' : undefined },
|
|
2221
|
+
];
|
|
2222
|
+
} else {
|
|
2223
|
+
const serviceSources = src.filter(m => m.type === 'service' || m.type === 'util' || m.type === 'other');
|
|
2224
|
+
items = [
|
|
2225
|
+
{ v: serviceSources.length, l: 'Источники логов' },
|
|
2226
|
+
{ v: '—', l: 'Коэффициент шума' },
|
|
2227
|
+
{ v: '—', l: 'Структурированность' },
|
|
2228
|
+
{ v: '—', l: 'Нет покрытия' },
|
|
2229
|
+
{ v: '—', l: 'Шумных паттернов' },
|
|
2230
|
+
];
|
|
2231
|
+
}
|
|
2211
2232
|
} else if (D.hasConfig && D.features) {
|
|
2212
2233
|
const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
|
|
2213
2234
|
items = [
|
|
@@ -2236,10 +2257,10 @@ function renderStats() {
|
|
|
2236
2257
|
|
|
2237
2258
|
function renderModeSwitch() {
|
|
2238
2259
|
const root = document.getElementById('modeSwitch');
|
|
2239
|
-
const modes = [
|
|
2240
|
-
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2241
|
-
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2242
|
-
];
|
|
2260
|
+
const modes = [
|
|
2261
|
+
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2262
|
+
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2263
|
+
];
|
|
2243
2264
|
root.innerHTML = modes.map(m => `
|
|
2244
2265
|
<button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
|
|
2245
2266
|
<span>${m.label}</span>
|
|
@@ -2257,15 +2278,15 @@ function renderSidebar() {
|
|
|
2257
2278
|
const tabs = document.getElementById('viewTabs');
|
|
2258
2279
|
const extra = document.getElementById('sidebarExtra');
|
|
2259
2280
|
|
|
2260
|
-
if (contextMode === 'observability') {
|
|
2261
|
-
tabs.style.display = 'none';
|
|
2262
|
-
extra.innerHTML = `
|
|
2263
|
-
<div class="sidebar-label">Фокус наблюдаемости</div>
|
|
2264
|
-
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
|
|
2265
|
-
Источники логов и качество сигналов для триажа инцидентов.
|
|
2266
|
-
</div>`;
|
|
2267
|
-
return;
|
|
2268
|
-
}
|
|
2281
|
+
if (contextMode === 'observability') {
|
|
2282
|
+
tabs.style.display = 'none';
|
|
2283
|
+
extra.innerHTML = `
|
|
2284
|
+
<div class="sidebar-label">Фокус наблюдаемости</div>
|
|
2285
|
+
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
|
|
2286
|
+
Источники логов и качество сигналов для триажа инцидентов.
|
|
2287
|
+
</div>`;
|
|
2288
|
+
return;
|
|
2289
|
+
}
|
|
2269
2290
|
|
|
2270
2291
|
tabs.style.display = 'flex';
|
|
2271
2292
|
document.querySelectorAll('.view-tab').forEach(t =>
|
|
@@ -2325,61 +2346,190 @@ function renderQaOnboarding() {
|
|
|
2325
2346
|
return `<div class="onboarding-block"><h3>QA Coverage: что это?</h3><p>Этот экран помогает найти пробелы в тестах: сначала проверь покрытие по фичам, затем открой критичные непокрытые файлы и запусти генерацию/фиксы тестов.</p></div>`;
|
|
2326
2347
|
}
|
|
2327
2348
|
|
|
2328
|
-
function renderObservability(c) {
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
const
|
|
2336
|
-
const
|
|
2337
|
-
const
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
|
|
2343
|
-
<
|
|
2344
|
-
|
|
2345
|
-
|
|
2346
|
-
|
|
2347
|
-
|
|
2348
|
-
|
|
2349
|
+
function renderObservability(c) {
|
|
2350
|
+
const o = D.observability;
|
|
2351
|
+
if (!o) {
|
|
2352
|
+
c.innerHTML = `<div class="onboarding-block"><h3>Наблюдаемость</h3><p>Данные анализа логов недоступны. Пересканируй проект.</p></div>`;
|
|
2353
|
+
return;
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
|
|
2357
|
+
const structPct = Math.round(o.metrics.structured_completeness * 100);
|
|
2358
|
+
const actionPct = Math.round(o.metrics.error_actionability * 100);
|
|
2359
|
+
const coveragePct = Math.round(o.metrics.coverage_of_key_flows * 100);
|
|
2360
|
+
|
|
2361
|
+
function metricColor(val, goodThreshold, warnThreshold, invert) {
|
|
2362
|
+
// invert=true: higher is worse (noise), invert=false: higher is better (coverage)
|
|
2363
|
+
if (invert) return val > warnThreshold ? 'var(--red)' : val > goodThreshold ? 'var(--yellow)' : 'var(--green)';
|
|
2364
|
+
return val < warnThreshold ? 'var(--red)' : val < goodThreshold ? 'var(--yellow)' : 'var(--green)';
|
|
2365
|
+
}
|
|
2366
|
+
|
|
2367
|
+
const formatLabels = { structured: 'structured', mixed: 'mixed', unstructured: 'unstructured' };
|
|
2368
|
+
const sourceByFormat = [
|
|
2369
|
+
{ label: 'Structured', count: o.catalog.filter(c => c.format === 'structured').length, color: 'var(--green)' },
|
|
2370
|
+
{ label: 'Mixed', count: o.catalog.filter(c => c.format === 'mixed').length, color: 'var(--yellow)' },
|
|
2371
|
+
{ label: 'Unstructured', count: o.catalog.filter(c => c.format === 'unstructured').length, color: 'var(--red)' },
|
|
2372
|
+
].filter(x => x.count > 0);
|
|
2373
|
+
|
|
2374
|
+
const recLabels = {
|
|
2375
|
+
'suppress': 'убрать',
|
|
2376
|
+
'downgrade level': 'понизить уровень',
|
|
2377
|
+
'enrich fields': 'обогатить поля',
|
|
2378
|
+
'add event': 'добавить событие',
|
|
2379
|
+
};
|
|
2380
|
+
const hasAgent = !!D.agent;
|
|
2381
|
+
|
|
2382
|
+
// Store catalog for buttons to reference by index (avoids inline JSON in onclick)
|
|
2383
|
+
window.__obsCatalog = o.catalog;
|
|
2384
|
+
|
|
2385
|
+
const noisyRows = o.topNoisyPatterns.slice(0, 8).map(i => {
|
|
2386
|
+
const safePattern = escapeHtml(i.pattern).replace(/'/g, ''');
|
|
2387
|
+
const btn = hasAgent
|
|
2388
|
+
? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-suppress-pattern',null,null,null,{pattern:'${safePattern}',recommendation:'${i.recommendation}'})">убрать</button>`
|
|
2389
|
+
: '';
|
|
2390
|
+
return `
|
|
2391
|
+
<div class="obs-list-item">
|
|
2392
|
+
<span class="obs-priority-${i.priority}" style="flex-shrink:0">[${i.priority}]</span>
|
|
2393
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(i.pattern)}">${escapeHtml(i.pattern)}</span>
|
|
2394
|
+
<strong style="flex-shrink:0;color:var(--red)">×${i.count} → ${recLabels[i.recommendation] || i.recommendation}</strong>
|
|
2395
|
+
${btn}
|
|
2396
|
+
</div>`;
|
|
2397
|
+
}).join('') || '<div class="obs-sub">Шумных паттернов не обнаружено</div>';
|
|
2398
|
+
|
|
2399
|
+
const missingRows = o.missingCriticalLogs.slice(0, 8).map(i => {
|
|
2400
|
+
const modulePath = i.pattern.split(':')[0].trim();
|
|
2401
|
+
const safePath = escapeHtml(modulePath).replace(/'/g, ''');
|
|
2402
|
+
const btn = hasAgent
|
|
2403
|
+
? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-add-critical-logs',null,null,null,{modulePath:'${safePath}'})">добавить</button>`
|
|
2404
|
+
: '';
|
|
2405
|
+
return `
|
|
2406
|
+
<div class="obs-list-item">
|
|
2407
|
+
<span class="obs-priority-${i.priority}" style="flex-shrink:0">[${i.priority}]</span>
|
|
2408
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(i.pattern)}">${escapeHtml(i.pattern)}</span>
|
|
2409
|
+
<strong style="flex-shrink:0;color:var(--yellow)">${recLabels[i.recommendation] || i.recommendation}</strong>
|
|
2410
|
+
${btn}
|
|
2411
|
+
</div>`;
|
|
2412
|
+
}).join('') || '<div class="obs-sub" style="color:var(--green)">Критичные сценарии покрыты</div>';
|
|
2413
|
+
|
|
2414
|
+
const fieldGaps = o.fieldGaps || {};
|
|
2415
|
+
const fieldGapEntries = Object.entries(fieldGaps).filter(([,v]) => v > 0).sort((a,b) => b[1] - a[1]);
|
|
2416
|
+
const fieldGapRows = fieldGapEntries.map(([name, count]) => {
|
|
2417
|
+
const btn = hasAgent
|
|
2418
|
+
? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-enrich-field',null,null,null,{fieldName:'${escapeHtml(name)}'})">обогатить</button>`
|
|
2419
|
+
: '';
|
|
2420
|
+
return `
|
|
2421
|
+
<div class="obs-list-item">
|
|
2422
|
+
<span><code>${escapeHtml(name)}</code></span>
|
|
2423
|
+
<strong style="color:${count > 20 ? 'var(--red)' : count > 5 ? 'var(--yellow)' : 'var(--muted)'}">${count} пропусков</strong>
|
|
2424
|
+
${btn}
|
|
2425
|
+
</div>`;
|
|
2426
|
+
}).join('') || '<div class="obs-sub" style="color:var(--green)">Все обязательные поля на месте</div>';
|
|
2427
|
+
|
|
2428
|
+
const catalogRows = o.catalog.slice(0, 15).map((i, idx) => {
|
|
2429
|
+
const missing = (i.missingFields || []);
|
|
2430
|
+
const missingStr = missing.length ? missing.join(', ') : '—';
|
|
2431
|
+
const btn = hasAgent
|
|
2432
|
+
? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-fix-module',null,null,null,{catalogIndex:${idx}})">исправить</button>`
|
|
2433
|
+
: '';
|
|
2434
|
+
return `
|
|
2435
|
+
<div class="obs-cat-row">
|
|
2436
|
+
<span title="${escapeHtml(i.modulePath)}">${escapeHtml(i.modulePath.split('/').slice(-2).join('/'))}</span>
|
|
2437
|
+
<span>${i.level}</span>
|
|
2438
|
+
<span style="color:${i.format==='structured'?'var(--green)':i.format==='mixed'?'var(--yellow)':'var(--red)'}">${i.format}</span>
|
|
2439
|
+
<span style="color:${missing.length > 4 ? 'var(--red)' : missing.length > 0 ? 'var(--yellow)' : 'var(--green)'}">${missing.length}/8</span>
|
|
2440
|
+
<span title="${escapeHtml(missingStr)}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(missingStr)}</span>
|
|
2441
|
+
<span style="color:${i.recommendation==='suppress'?'var(--red)':i.recommendation==='add event'?'var(--yellow)':'var(--muted)'}">${recLabels[i.recommendation] || i.recommendation}</span>
|
|
2442
|
+
${btn}
|
|
2443
|
+
</div>`}).join('');
|
|
2444
|
+
|
|
2445
|
+
c.innerHTML = `
|
|
2446
|
+
<div class="onboarding-block">
|
|
2447
|
+
<h3>Наблюдаемость: что это?</h3>
|
|
2448
|
+
<p>Аудит покрытия логами: что добавить, что убрать, что обогатить — на основе статического анализа лог-вызовов.</p>
|
|
2449
|
+
</div>
|
|
2450
|
+
|
|
2451
|
+
<div class="obs-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
|
|
2452
|
+
<div class="obs-card">
|
|
2453
|
+
<div class="obs-title">Коэффициент шума</div>
|
|
2454
|
+
<div class="obs-value" style="color:${metricColor(noiseRatio,10,30,true)}">${noiseRatio}%</div>
|
|
2455
|
+
<div class="obs-sub">Доля шумных лог-вызовов из всех.</div>
|
|
2456
|
+
</div>
|
|
2349
2457
|
<div class="obs-card">
|
|
2350
|
-
<div class="obs-title"
|
|
2458
|
+
<div class="obs-title">Структурированность</div>
|
|
2459
|
+
<div class="obs-value" style="color:${metricColor(structPct,80,50,false)}">${structPct}%</div>
|
|
2460
|
+
<div class="obs-sub">Логи с обязательными полями (module, event, traceId).</div>
|
|
2461
|
+
</div>
|
|
2462
|
+
<div class="obs-card">
|
|
2463
|
+
<div class="obs-title">Actionable ошибки</div>
|
|
2464
|
+
<div class="obs-value" style="color:${metricColor(actionPct,80,50,false)}">${actionPct}%</div>
|
|
2465
|
+
<div class="obs-sub">ERROR-логи с контекстом для диагностики.</div>
|
|
2466
|
+
</div>
|
|
2467
|
+
<div class="obs-card">
|
|
2468
|
+
<div class="obs-title">Покрытие сценариев</div>
|
|
2469
|
+
<div class="obs-value" style="color:${metricColor(coveragePct,80,50,false)}">${coveragePct}%</div>
|
|
2470
|
+
<div class="obs-sub">Модули с хотя бы одним warn/error событием.</div>
|
|
2471
|
+
</div>
|
|
2472
|
+
</div>
|
|
2473
|
+
|
|
2474
|
+
<div class="obs-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:12px">
|
|
2475
|
+
<div class="obs-card">
|
|
2476
|
+
<div class="obs-title">Источники по формату</div>
|
|
2351
2477
|
<div class="obs-list">
|
|
2352
|
-
${
|
|
2478
|
+
${sourceByFormat.map(g => `
|
|
2479
|
+
<div class="obs-list-item">
|
|
2480
|
+
<span style="color:${g.color}">${g.label}</span>
|
|
2481
|
+
<strong>${g.count}</strong>
|
|
2482
|
+
</div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
|
|
2353
2483
|
</div>
|
|
2354
2484
|
</div>
|
|
2485
|
+
<div class="obs-card">
|
|
2486
|
+
<div class="obs-title">Классификация логов</div>
|
|
2487
|
+
<div class="obs-list">
|
|
2488
|
+
<div class="obs-list-item"><span style="color:var(--red)">Мусор (убрать)</span><strong>${o.classification.trash}</strong></div>
|
|
2489
|
+
<div class="obs-list-item"><span style="color:var(--green)">Полезные</span><strong>${o.classification.useful}</strong></div>
|
|
2490
|
+
<div class="obs-list-item"><span style="color:var(--blue)">Критичные</span><strong>${o.classification.critical}</strong></div>
|
|
2491
|
+
</div>
|
|
2492
|
+
</div>
|
|
2493
|
+
<div class="obs-card">
|
|
2494
|
+
<div class="obs-title">Рекомендации</div>
|
|
2495
|
+
<div class="obs-list">
|
|
2496
|
+
${['suppress','enrich fields','add event','downgrade level'].map(rec => {
|
|
2497
|
+
const count = o.catalog.filter(c => c.recommendation === rec).length;
|
|
2498
|
+
if (!count) return '';
|
|
2499
|
+
const btn = hasAgent
|
|
2500
|
+
? `<button class="obs-action-btn obs-batch-btn" onclick="event.stopPropagation();runAgentTask('obs-batch-recommendation',null,null,null,{recommendationType:'${rec}'})">исправить все</button>`
|
|
2501
|
+
: '';
|
|
2502
|
+
return `<div class="obs-list-item"><span>${recLabels[rec]}</span><strong>${count}</strong>${btn}</div>`;
|
|
2503
|
+
}).join('')}
|
|
2504
|
+
</div>
|
|
2505
|
+
</div>
|
|
2506
|
+
</div>
|
|
2355
2507
|
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
<div class="obs-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
<div class="obs-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
<div class="obs-
|
|
2376
|
-
<div class="obs-
|
|
2377
|
-
<div class="obs-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
</div>`;
|
|
2382
|
-
}
|
|
2508
|
+
<div class="obs-grid" style="grid-template-columns:1fr 1fr;margin-bottom:12px">
|
|
2509
|
+
<div class="obs-card">
|
|
2510
|
+
<div class="obs-title">Что убрать — шумные паттерны</div>
|
|
2511
|
+
<div class="obs-list" style="gap:4px">${noisyRows}</div>
|
|
2512
|
+
</div>
|
|
2513
|
+
<div class="obs-card">
|
|
2514
|
+
<div class="obs-title">Что добавить — нет критичных логов</div>
|
|
2515
|
+
<div class="obs-list" style="gap:4px">${missingRows}</div>
|
|
2516
|
+
</div>
|
|
2517
|
+
</div>
|
|
2518
|
+
|
|
2519
|
+
<div class="obs-card" style="margin-bottom:12px">
|
|
2520
|
+
<div class="obs-title">Что обогатить — пробелы по полям</div>
|
|
2521
|
+
<div class="obs-sub" style="margin-bottom:8px">Обязательные поля по стандарту: service, env, trace_id, request_id, event_name, outcome, error_code (warn/error), user_id.</div>
|
|
2522
|
+
<div class="obs-list">${fieldGapRows}</div>
|
|
2523
|
+
</div>
|
|
2524
|
+
|
|
2525
|
+
<div class="obs-card" style="margin-bottom:12px">
|
|
2526
|
+
<div class="obs-title">Каталог источников логов (топ 15)</div>
|
|
2527
|
+
<div class="obs-catalog" style="margin-top:8px;border:none;padding:0">
|
|
2528
|
+
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>пробелы</span><span>не хватает</span><span>действие</span><span></span></div>
|
|
2529
|
+
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2530
|
+
</div>
|
|
2531
|
+
</div>`;
|
|
2532
|
+
}
|
|
2383
2533
|
|
|
2384
2534
|
function backToFeatureDetail() {
|
|
2385
2535
|
drillTestType = null;
|
|
@@ -2397,15 +2547,15 @@ function toPct(v) {
|
|
|
2397
2547
|
return Math.round((v || 0) * 100) + '%';
|
|
2398
2548
|
}
|
|
2399
2549
|
|
|
2400
|
-
function renderObservabilityOverview(c) {
|
|
2401
|
-
const o = D.observability;
|
|
2402
|
-
if (!o) return '';
|
|
2403
|
-
const metrics = [
|
|
2404
|
-
['Коэффициент шума', toPct(o.metrics.noise_ratio)],
|
|
2405
|
-
['Действия по ошибкам', toPct(o.metrics.error_actionability)],
|
|
2406
|
-
['Полнота структуры', toPct(o.metrics.structured_completeness)],
|
|
2407
|
-
['Покрытие ключевых сценариев', toPct(o.metrics.coverage_of_key_flows)],
|
|
2408
|
-
];
|
|
2550
|
+
function renderObservabilityOverview(c) {
|
|
2551
|
+
const o = D.observability;
|
|
2552
|
+
if (!o) return '';
|
|
2553
|
+
const metrics = [
|
|
2554
|
+
['Коэффициент шума', toPct(o.metrics.noise_ratio)],
|
|
2555
|
+
['Действия по ошибкам', toPct(o.metrics.error_actionability)],
|
|
2556
|
+
['Полнота структуры', toPct(o.metrics.structured_completeness)],
|
|
2557
|
+
['Покрытие ключевых сценариев', toPct(o.metrics.coverage_of_key_flows)],
|
|
2558
|
+
];
|
|
2409
2559
|
const noisy = (o.topNoisyPatterns || []).slice(0, 5).map(i =>
|
|
2410
2560
|
`<div class="obs-row"><span class="obs-priority-${i.priority}">[${i.priority}]</span> ${escapeHtml(i.pattern)} · x${i.count} → <b>${i.recommendation}</b></div>`
|
|
2411
2561
|
).join('') || '<div class="obs-row">Нет шумных паттернов</div>';
|
|
@@ -2418,27 +2568,27 @@ function renderObservabilityOverview(c) {
|
|
|
2418
2568
|
`<div class="obs-cat-row"><span>${escapeHtml(i.modulePath)}</span><span>${i.level}</span><span>${i.format}</span><span>${i.frequency}</span><span>${escapeHtml(i.owner)}</span><span>${i.recommendation}</span></div>`
|
|
2419
2569
|
).join('');
|
|
2420
2570
|
|
|
2421
|
-
return `
|
|
2422
|
-
<div class="observability-panel">
|
|
2423
|
-
<div class="observability-title">Наблюдаемость</div>
|
|
2424
|
-
<div class="obs-metrics">
|
|
2425
|
-
${metrics.map(([l,v]) => `<div class="obs-metric"><div class="obs-metric-v">${v}</div><div class="obs-metric-l">${l}</div></div>`).join('')}
|
|
2426
|
-
</div>
|
|
2427
|
-
<div class="obs-columns">
|
|
2428
|
-
<div class="obs-list">
|
|
2429
|
-
<h4>Топ шумных паттернов</h4>
|
|
2430
|
-
${noisy}
|
|
2431
|
-
</div>
|
|
2432
|
-
<div class="obs-list">
|
|
2433
|
-
<h4>Отсутствующие критичные логи</h4>
|
|
2434
|
-
${missing}
|
|
2435
|
-
</div>
|
|
2436
|
-
</div>
|
|
2437
|
-
<div class="obs-catalog">
|
|
2438
|
-
<h4>Каталог источников логов (топ 10)</h4>
|
|
2439
|
-
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>частота</span><span>владелец</span><span>действие</span></div>
|
|
2440
|
-
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2441
|
-
</div>
|
|
2571
|
+
return `
|
|
2572
|
+
<div class="observability-panel">
|
|
2573
|
+
<div class="observability-title">Наблюдаемость</div>
|
|
2574
|
+
<div class="obs-metrics">
|
|
2575
|
+
${metrics.map(([l,v]) => `<div class="obs-metric"><div class="obs-metric-v">${v}</div><div class="obs-metric-l">${l}</div></div>`).join('')}
|
|
2576
|
+
</div>
|
|
2577
|
+
<div class="obs-columns">
|
|
2578
|
+
<div class="obs-list">
|
|
2579
|
+
<h4>Топ шумных паттернов</h4>
|
|
2580
|
+
${noisy}
|
|
2581
|
+
</div>
|
|
2582
|
+
<div class="obs-list">
|
|
2583
|
+
<h4>Отсутствующие критичные логи</h4>
|
|
2584
|
+
${missing}
|
|
2585
|
+
</div>
|
|
2586
|
+
</div>
|
|
2587
|
+
<div class="obs-catalog">
|
|
2588
|
+
<h4>Каталог источников логов (топ 10)</h4>
|
|
2589
|
+
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>частота</span><span>владелец</span><span>действие</span></div>
|
|
2590
|
+
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2591
|
+
</div>
|
|
2442
2592
|
<div class="obs-row" style="margin-top:8px">
|
|
2443
2593
|
Классификация: мусор ${o.classification.trash} · полезно ${o.classification.useful} · критично ${o.classification.critical}
|
|
2444
2594
|
</div>
|
|
@@ -2476,7 +2626,7 @@ function renderFeatureCards(c) {
|
|
|
2476
2626
|
</div>
|
|
2477
2627
|
</div>` : '';
|
|
2478
2628
|
|
|
2479
|
-
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
2629
|
+
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
2480
2630
|
const grid = document.getElementById('featGrid');
|
|
2481
2631
|
|
|
2482
2632
|
list.forEach(f => {
|
|
@@ -2781,12 +2931,12 @@ function renderFeatureDetail(c) {
|
|
|
2781
2931
|
return;
|
|
2782
2932
|
}
|
|
2783
2933
|
|
|
2784
|
-
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
2785
|
-
const src = mods.filter(m => m.type !== 'test');
|
|
2786
|
-
const untestedSrc = src.filter(m => !m.hasTests);
|
|
2787
|
-
const tst = mods.filter(m => m.type === 'test');
|
|
2788
|
-
const testedCount = src.filter(m => m.hasTests).length;
|
|
2789
|
-
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
2934
|
+
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
2935
|
+
const src = mods.filter(m => m.type !== 'test');
|
|
2936
|
+
const untestedSrc = src.filter(m => !m.hasTests);
|
|
2937
|
+
const tst = mods.filter(m => m.type === 'test');
|
|
2938
|
+
const testedCount = src.filter(m => m.hasTests).length;
|
|
2939
|
+
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
2790
2940
|
|
|
2791
2941
|
const unitCount = feat.unitTestCount ?? tst.filter(m => m.testType === 'unit').length;
|
|
2792
2942
|
const integrationCount = feat.integrationTestCount ?? tst.filter(m => m.testType === 'integration').length;
|
|
@@ -2798,32 +2948,32 @@ function renderFeatureDetail(c) {
|
|
|
2798
2948
|
const integrationFailed = tst.filter(m => m.testType === 'integration' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
2799
2949
|
const e2eFailed = tst.filter(m => m.testType === 'e2e' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
2800
2950
|
|
|
2801
|
-
// Determine what list to show based on active tab
|
|
2802
|
-
// null or 'source' → source files; test type → test files of that type
|
|
2803
|
-
const activeTab = drillTestType || 'source';
|
|
2804
|
-
const sourceList = showOnlyUntestedInFeature ? untestedSrc : src;
|
|
2805
|
-
const listFiles = activeTab === 'source' ? sourceList : tst.filter(m => m.testType === activeTab);
|
|
2806
|
-
const isTestList = activeTab !== 'source';
|
|
2807
|
-
const showUntestedToggle = activeTab === 'source';
|
|
2808
|
-
const meta = TEST_TYPE_META[activeTab];
|
|
2809
|
-
const listLabel = meta
|
|
2810
|
-
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
2811
|
-
: `📁 Файлы фичи (${listFiles.length})`;
|
|
2812
|
-
|
|
2813
|
-
const q = searchQuery.toLowerCase();
|
|
2814
|
-
const filtered = q ? listFiles.filter(m =>
|
|
2815
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2816
|
-
) : listFiles;
|
|
2817
|
-
const rowsRenderKey = `feature:${drillFeatureKey}:${activeTab}:${showOnlyUntestedInFeature ? 1 : 0}:${q}`;
|
|
2818
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2819
|
-
const selectableSource = !isTestList && !!D.agent;
|
|
2820
|
-
const visibleSourcePaths = selectableSource
|
|
2821
|
-
? visibleRows.map(m => m.relativePath.replace(/\\/g, '/'))
|
|
2822
|
-
: [];
|
|
2823
|
-
const selectedVisible = selectableSource
|
|
2824
|
-
? selectedFilesForFeature(drillFeatureKey, visibleRows.map(m => m.relativePath))
|
|
2825
|
-
: [];
|
|
2826
|
-
const selectedCount = selectedVisible.length;
|
|
2951
|
+
// Determine what list to show based on active tab
|
|
2952
|
+
// null or 'source' → source files; test type → test files of that type
|
|
2953
|
+
const activeTab = drillTestType || 'source';
|
|
2954
|
+
const sourceList = showOnlyUntestedInFeature ? untestedSrc : src;
|
|
2955
|
+
const listFiles = activeTab === 'source' ? sourceList : tst.filter(m => m.testType === activeTab);
|
|
2956
|
+
const isTestList = activeTab !== 'source';
|
|
2957
|
+
const showUntestedToggle = activeTab === 'source';
|
|
2958
|
+
const meta = TEST_TYPE_META[activeTab];
|
|
2959
|
+
const listLabel = meta
|
|
2960
|
+
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
2961
|
+
: `📁 Файлы фичи (${listFiles.length})`;
|
|
2962
|
+
|
|
2963
|
+
const q = searchQuery.toLowerCase();
|
|
2964
|
+
const filtered = q ? listFiles.filter(m =>
|
|
2965
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2966
|
+
) : listFiles;
|
|
2967
|
+
const rowsRenderKey = `feature:${drillFeatureKey}:${activeTab}:${showOnlyUntestedInFeature ? 1 : 0}:${q}`;
|
|
2968
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2969
|
+
const selectableSource = !isTestList && !!D.agent;
|
|
2970
|
+
const visibleSourcePaths = selectableSource
|
|
2971
|
+
? visibleRows.map(m => m.relativePath.replace(/\\/g, '/'))
|
|
2972
|
+
: [];
|
|
2973
|
+
const selectedVisible = selectableSource
|
|
2974
|
+
? selectedFilesForFeature(drillFeatureKey, visibleRows.map(m => m.relativePath))
|
|
2975
|
+
: [];
|
|
2976
|
+
const selectedCount = selectedVisible.length;
|
|
2827
2977
|
|
|
2828
2978
|
c.innerHTML = `
|
|
2829
2979
|
<div class="drill-header">
|
|
@@ -2849,51 +2999,51 @@ function renderFeatureDetail(c) {
|
|
|
2849
2999
|
${testTypeCard('integration', 'Integration', '🔗', '#58a6ff', integrationCount, activeTab === 'integration', drillFeatureKey, integrationFailed)}
|
|
2850
3000
|
${testTypeCard('e2e', 'E2E', '🎭', '#d2a8ff', e2eCount, activeTab === 'e2e', drillFeatureKey, e2eFailed)}
|
|
2851
3001
|
</div>
|
|
2852
|
-
|
|
2853
|
-
<div class="drill-section-label">${listLabel}</div>
|
|
2854
|
-
${showUntestedToggle ? `
|
|
2855
|
-
<div class="feature-file-filters">
|
|
2856
|
-
<label class="feature-filter-toggle">
|
|
2857
|
-
<input type="checkbox" id="untestedOnlyToggle" ${showOnlyUntestedInFeature ? 'checked' : ''}>
|
|
2858
|
-
<span>Только без тестов</span>
|
|
2859
|
-
<span class="feature-filter-meta">(${untestedSrc.length})</span>
|
|
2860
|
-
</label>
|
|
2861
|
-
</div>` : ''}
|
|
2862
|
-
${selectableSource ? `
|
|
2863
|
-
<div class="bulk-actions">
|
|
2864
|
-
<span class="bulk-actions-count">Выбрано файлов: <b>${selectedCount}</b></span>
|
|
2865
|
-
<button class="bulk-actions-btn" id="bulkSelectAllBtn" ${visibleSourcePaths.length === 0 ? 'disabled' : ''}>☑ Выбрать все (${visibleSourcePaths.length})</button>
|
|
3002
|
+
|
|
3003
|
+
<div class="drill-section-label">${listLabel}</div>
|
|
3004
|
+
${showUntestedToggle ? `
|
|
3005
|
+
<div class="feature-file-filters">
|
|
3006
|
+
<label class="feature-filter-toggle">
|
|
3007
|
+
<input type="checkbox" id="untestedOnlyToggle" ${showOnlyUntestedInFeature ? 'checked' : ''}>
|
|
3008
|
+
<span>Только без тестов</span>
|
|
3009
|
+
<span class="feature-filter-meta">(${untestedSrc.length})</span>
|
|
3010
|
+
</label>
|
|
3011
|
+
</div>` : ''}
|
|
3012
|
+
${selectableSource ? `
|
|
3013
|
+
<div class="bulk-actions">
|
|
3014
|
+
<span class="bulk-actions-count">Выбрано файлов: <b>${selectedCount}</b></span>
|
|
3015
|
+
<button class="bulk-actions-btn" id="bulkSelectAllBtn" ${visibleSourcePaths.length === 0 ? 'disabled' : ''}>☑ Выбрать все (${visibleSourcePaths.length})</button>
|
|
2866
3016
|
<button class="bulk-actions-btn" id="bulkClearBtn" ${selectedCount === 0 ? 'disabled' : ''}>Снять выбор</button>
|
|
2867
3017
|
<button class="bulk-actions-btn primary" id="bulkWriteTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>✍ Написать тесты для выбранных</button>
|
|
2868
3018
|
<button class="bulk-actions-btn" id="bulkRefreshTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>↻ Актуализировать тесты</button>
|
|
2869
3019
|
</div>` : ''}
|
|
2870
|
-
<div class="file-rows" id="fileRows">
|
|
2871
|
-
${filtered.length === 0
|
|
2872
|
-
? `<div style="padding:20px;text-align:center;border:1px dashed var(--border);border-radius:8px;color:var(--dim);font-size:13px">
|
|
2873
|
-
${isTestList
|
|
2874
|
-
? 'Нет тестов этого типа для данной фичи'
|
|
2875
|
-
: (showOnlyUntestedInFeature
|
|
2876
|
-
? 'Все файлы этой фичи уже покрыты тестами'
|
|
2877
|
-
: 'Нет файлов — возможно паттерны в конфиге не совпадают')}
|
|
2878
|
-
</div>`
|
|
2879
|
-
: visibleRows.map(m => fileRow(m, isTestList, drillFeatureKey, selectableSource)).join('')
|
|
2880
|
-
}
|
|
2881
|
-
</div>
|
|
2882
|
-
${hasMoreRows ? `
|
|
2883
|
-
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
2884
|
-
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
2885
|
-
<button class="bulk-actions-btn" id="fileRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
2886
|
-
</div>` : ''}`;
|
|
2887
|
-
|
|
2888
|
-
const untestedOnlyToggle = document.getElementById('untestedOnlyToggle');
|
|
2889
|
-
if (untestedOnlyToggle) {
|
|
2890
|
-
untestedOnlyToggle.onchange = (e) => {
|
|
2891
|
-
showOnlyUntestedInFeature = !!e.target.checked;
|
|
2892
|
-
renderContent();
|
|
2893
|
-
};
|
|
2894
|
-
}
|
|
2895
|
-
|
|
2896
|
-
if (selectableSource) {
|
|
3020
|
+
<div class="file-rows" id="fileRows">
|
|
3021
|
+
${filtered.length === 0
|
|
3022
|
+
? `<div style="padding:20px;text-align:center;border:1px dashed var(--border);border-radius:8px;color:var(--dim);font-size:13px">
|
|
3023
|
+
${isTestList
|
|
3024
|
+
? 'Нет тестов этого типа для данной фичи'
|
|
3025
|
+
: (showOnlyUntestedInFeature
|
|
3026
|
+
? 'Все файлы этой фичи уже покрыты тестами'
|
|
3027
|
+
: 'Нет файлов — возможно паттерны в конфиге не совпадают')}
|
|
3028
|
+
</div>`
|
|
3029
|
+
: visibleRows.map(m => fileRow(m, isTestList, drillFeatureKey, selectableSource)).join('')
|
|
3030
|
+
}
|
|
3031
|
+
</div>
|
|
3032
|
+
${hasMoreRows ? `
|
|
3033
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3034
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
3035
|
+
<button class="bulk-actions-btn" id="fileRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3036
|
+
</div>` : ''}`;
|
|
3037
|
+
|
|
3038
|
+
const untestedOnlyToggle = document.getElementById('untestedOnlyToggle');
|
|
3039
|
+
if (untestedOnlyToggle) {
|
|
3040
|
+
untestedOnlyToggle.onchange = (e) => {
|
|
3041
|
+
showOnlyUntestedInFeature = !!e.target.checked;
|
|
3042
|
+
renderContent();
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
if (selectableSource) {
|
|
2897
3047
|
const allVisibleSelected = visibleSourcePaths.length > 0 && visibleSourcePaths.every((p) => selectedSourceFiles.has(p));
|
|
2898
3048
|
const bulkSelectAllBtn = document.getElementById('bulkSelectAllBtn');
|
|
2899
3049
|
if (bulkSelectAllBtn) {
|
|
@@ -2928,17 +3078,17 @@ function renderFeatureDetail(c) {
|
|
|
2928
3078
|
renderContent();
|
|
2929
3079
|
};
|
|
2930
3080
|
}
|
|
2931
|
-
}
|
|
2932
|
-
|
|
2933
|
-
const fileRowsLoadMoreBtn = document.getElementById('fileRowsLoadMoreBtn');
|
|
2934
|
-
if (fileRowsLoadMoreBtn) {
|
|
2935
|
-
fileRowsLoadMoreBtn.onclick = () => {
|
|
2936
|
-
increaseFileRowsLimit();
|
|
2937
|
-
renderContent();
|
|
2938
|
-
};
|
|
2939
|
-
}
|
|
2940
|
-
|
|
2941
|
-
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
3081
|
+
}
|
|
3082
|
+
|
|
3083
|
+
const fileRowsLoadMoreBtn = document.getElementById('fileRowsLoadMoreBtn');
|
|
3084
|
+
if (fileRowsLoadMoreBtn) {
|
|
3085
|
+
fileRowsLoadMoreBtn.onclick = () => {
|
|
3086
|
+
increaseFileRowsLimit();
|
|
3087
|
+
renderContent();
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
2942
3092
|
card.onclick = async () => {
|
|
2943
3093
|
const type = card.dataset.testtype;
|
|
2944
3094
|
drillTestType = (type === 'source') ? null : type; // 'source' tab = null state
|
|
@@ -2947,11 +3097,11 @@ function renderFeatureDetail(c) {
|
|
|
2947
3097
|
if (type === 'e2e') {
|
|
2948
3098
|
await loadE2ePlan(drillFeatureKey);
|
|
2949
3099
|
}
|
|
2950
|
-
renderContent();
|
|
2951
|
-
};
|
|
2952
|
-
});
|
|
2953
|
-
bindFileRowsClick(c);
|
|
2954
|
-
}
|
|
3100
|
+
renderContent();
|
|
3101
|
+
};
|
|
3102
|
+
});
|
|
3103
|
+
bindFileRowsClick(c);
|
|
3104
|
+
}
|
|
2955
3105
|
|
|
2956
3106
|
const TEST_TYPE_META = {
|
|
2957
3107
|
unit: { label: 'Unit', icon: '🧪', color: '#e3b341', desc: 'Изолированные тесты функций и модулей' },
|
|
@@ -2970,12 +3120,12 @@ function renderTestTypeDetail(c) {
|
|
|
2970
3120
|
m.featureKeys && m.featureKeys.includes(drillFeatureKey)
|
|
2971
3121
|
);
|
|
2972
3122
|
|
|
2973
|
-
const q = searchQuery.toLowerCase();
|
|
2974
|
-
const filtered = q ? tests.filter(m =>
|
|
2975
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2976
|
-
) : tests;
|
|
2977
|
-
const rowsRenderKey = `tests:${drillFeatureKey}:${drillTestType}:${q}`;
|
|
2978
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3123
|
+
const q = searchQuery.toLowerCase();
|
|
3124
|
+
const filtered = q ? tests.filter(m =>
|
|
3125
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3126
|
+
) : tests;
|
|
3127
|
+
const rowsRenderKey = `tests:${drillFeatureKey}:${drillTestType}:${q}`;
|
|
3128
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2979
3129
|
|
|
2980
3130
|
c.innerHTML = `
|
|
2981
3131
|
<div class="drill-header">
|
|
@@ -2990,32 +3140,32 @@ function renderTestTypeDetail(c) {
|
|
|
2990
3140
|
</div>
|
|
2991
3141
|
<div style="font-size:12px;color:var(--dim);margin-bottom:16px">${meta.desc}</div>
|
|
2992
3142
|
|
|
2993
|
-
<div class="file-rows" id="fileRows">
|
|
2994
|
-
${filtered.length === 0
|
|
2995
|
-
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
2996
|
-
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
2997
|
-
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
2998
|
-
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
2999
|
-
</div>`
|
|
3000
|
-
: visibleRows.map(m => fileRow(m, true)).join('')
|
|
3001
|
-
}
|
|
3002
|
-
</div>
|
|
3003
|
-
${hasMoreRows ? `
|
|
3004
|
-
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3005
|
-
<span>Показано ${visibleRows.length} из ${filtered.length} тест-файлов</span>
|
|
3006
|
-
<button class="bulk-actions-btn" id="testRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3007
|
-
</div>` : ''}`;
|
|
3008
|
-
|
|
3009
|
-
const testRowsLoadMoreBtn = document.getElementById('testRowsLoadMoreBtn');
|
|
3010
|
-
if (testRowsLoadMoreBtn) {
|
|
3011
|
-
testRowsLoadMoreBtn.onclick = () => {
|
|
3012
|
-
increaseFileRowsLimit();
|
|
3013
|
-
renderContent();
|
|
3014
|
-
};
|
|
3015
|
-
}
|
|
3016
|
-
|
|
3017
|
-
bindFileRowsClick(c);
|
|
3018
|
-
}
|
|
3143
|
+
<div class="file-rows" id="fileRows">
|
|
3144
|
+
${filtered.length === 0
|
|
3145
|
+
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
3146
|
+
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
3147
|
+
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
3148
|
+
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
3149
|
+
</div>`
|
|
3150
|
+
: visibleRows.map(m => fileRow(m, true)).join('')
|
|
3151
|
+
}
|
|
3152
|
+
</div>
|
|
3153
|
+
${hasMoreRows ? `
|
|
3154
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3155
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} тест-файлов</span>
|
|
3156
|
+
<button class="bulk-actions-btn" id="testRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3157
|
+
</div>` : ''}`;
|
|
3158
|
+
|
|
3159
|
+
const testRowsLoadMoreBtn = document.getElementById('testRowsLoadMoreBtn');
|
|
3160
|
+
if (testRowsLoadMoreBtn) {
|
|
3161
|
+
testRowsLoadMoreBtn.onclick = () => {
|
|
3162
|
+
increaseFileRowsLimit();
|
|
3163
|
+
renderContent();
|
|
3164
|
+
};
|
|
3165
|
+
}
|
|
3166
|
+
|
|
3167
|
+
bindFileRowsClick(c);
|
|
3168
|
+
}
|
|
3019
3169
|
|
|
3020
3170
|
function renderUnmappedDetail(c) {
|
|
3021
3171
|
const infraSrc = D.modules.filter(m => m.type !== 'test' && m.isInfra);
|
|
@@ -3023,12 +3173,12 @@ function renderUnmappedDetail(c) {
|
|
|
3023
3173
|
m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)
|
|
3024
3174
|
);
|
|
3025
3175
|
|
|
3026
|
-
const q = searchQuery.toLowerCase();
|
|
3027
|
-
const filtered = q ? unmappedSrc.filter(m =>
|
|
3028
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3029
|
-
) : unmappedSrc;
|
|
3030
|
-
const rowsRenderKey = `unmapped:${q}`;
|
|
3031
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3176
|
+
const q = searchQuery.toLowerCase();
|
|
3177
|
+
const filtered = q ? unmappedSrc.filter(m =>
|
|
3178
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3179
|
+
) : unmappedSrc;
|
|
3180
|
+
const rowsRenderKey = `unmapped:${q}`;
|
|
3181
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3032
3182
|
|
|
3033
3183
|
// Build prompt text
|
|
3034
3184
|
const featureList = (D.features || []).map(f => ` • ${f.key} — ${f.label}`).join('\n');
|
|
@@ -3069,44 +3219,44 @@ function renderUnmappedDetail(c) {
|
|
|
3069
3219
|
border-radius:6px; color:var(--blue); font-size:12px; cursor:pointer;
|
|
3070
3220
|
">📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)</button>
|
|
3071
3221
|
</div>
|
|
3072
|
-
<div class="file-rows" id="fileRows">
|
|
3073
|
-
${filtered.length === 0
|
|
3074
|
-
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
3075
|
-
: visibleRows.map(m => fileRow(m)).join('')
|
|
3076
|
-
}
|
|
3077
|
-
</div>
|
|
3078
|
-
${hasMoreRows ? `
|
|
3079
|
-
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3080
|
-
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
3081
|
-
<button class="bulk-actions-btn" id="unmappedRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3082
|
-
</div>` : ''}`;
|
|
3222
|
+
<div class="file-rows" id="fileRows">
|
|
3223
|
+
${filtered.length === 0
|
|
3224
|
+
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
3225
|
+
: visibleRows.map(m => fileRow(m)).join('')
|
|
3226
|
+
}
|
|
3227
|
+
</div>
|
|
3228
|
+
${hasMoreRows ? `
|
|
3229
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3230
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
3231
|
+
<button class="bulk-actions-btn" id="unmappedRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3232
|
+
</div>` : ''}`;
|
|
3083
3233
|
|
|
3084
3234
|
const runAgentUnmappedBtn = document.getElementById('runAgentUnmapped');
|
|
3085
3235
|
if (runAgentUnmappedBtn) {
|
|
3086
3236
|
runAgentUnmappedBtn.onclick = () => runAgentTask('map-unmapped');
|
|
3087
3237
|
}
|
|
3088
3238
|
|
|
3089
|
-
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
3239
|
+
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
3090
3240
|
navigator.clipboard.writeText(promptText).then(() => {
|
|
3091
3241
|
this.textContent = '✅ Скопировано!';
|
|
3092
3242
|
this.style.color = 'var(--green)';
|
|
3093
3243
|
setTimeout(() => {
|
|
3094
3244
|
this.textContent = `📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)`;
|
|
3095
3245
|
this.style.color = 'var(--blue)';
|
|
3096
|
-
}, 3000);
|
|
3097
|
-
});
|
|
3098
|
-
};
|
|
3099
|
-
|
|
3100
|
-
const unmappedRowsLoadMoreBtn = document.getElementById('unmappedRowsLoadMoreBtn');
|
|
3101
|
-
if (unmappedRowsLoadMoreBtn) {
|
|
3102
|
-
unmappedRowsLoadMoreBtn.onclick = () => {
|
|
3103
|
-
increaseFileRowsLimit();
|
|
3104
|
-
renderContent();
|
|
3105
|
-
};
|
|
3106
|
-
}
|
|
3107
|
-
|
|
3108
|
-
bindFileRowsClick(c);
|
|
3109
|
-
}
|
|
3246
|
+
}, 3000);
|
|
3247
|
+
});
|
|
3248
|
+
};
|
|
3249
|
+
|
|
3250
|
+
const unmappedRowsLoadMoreBtn = document.getElementById('unmappedRowsLoadMoreBtn');
|
|
3251
|
+
if (unmappedRowsLoadMoreBtn) {
|
|
3252
|
+
unmappedRowsLoadMoreBtn.onclick = () => {
|
|
3253
|
+
increaseFileRowsLimit();
|
|
3254
|
+
renderContent();
|
|
3255
|
+
};
|
|
3256
|
+
}
|
|
3257
|
+
|
|
3258
|
+
bindFileRowsClick(c);
|
|
3259
|
+
}
|
|
3110
3260
|
|
|
3111
3261
|
function fileRow(m, isTest = false, featureKey = null, selectable = false) {
|
|
3112
3262
|
const relPath = m.relativePath.replace(/\\/g, '/');
|
|
@@ -3196,33 +3346,33 @@ function toggleSourceSelection(relPath, checked) {
|
|
|
3196
3346
|
renderContent();
|
|
3197
3347
|
}
|
|
3198
3348
|
|
|
3199
|
-
function renderModuleGrid(c) {
|
|
3200
|
-
const q = searchQuery.toLowerCase();
|
|
3201
|
-
const list = D.modules.filter(m => {
|
|
3202
|
-
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
3203
|
-
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
3204
|
-
return true;
|
|
3205
|
-
});
|
|
3206
|
-
const typeKey = [...activeTypes].sort().join(',');
|
|
3207
|
-
const rowsRenderKey = `modules:${contextMode}:${view}:${typeKey}:${q}`;
|
|
3208
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(list, rowsRenderKey);
|
|
3209
|
-
|
|
3210
|
-
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
3211
|
-
|
|
3212
|
-
c.innerHTML = `
|
|
3213
|
-
<div class="module-grid" id="modGrid"></div>
|
|
3214
|
-
${hasMoreRows ? `
|
|
3215
|
-
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3216
|
-
<span>Показано ${visibleRows.length} из ${list.length} модулей</span>
|
|
3217
|
-
<button class="bulk-actions-btn" id="moduleRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3218
|
-
</div>` : ''}
|
|
3219
|
-
`;
|
|
3220
|
-
const grid = document.getElementById('modGrid');
|
|
3221
|
-
|
|
3222
|
-
visibleRows.forEach(m => {
|
|
3223
|
-
const cov = m.coverage?.lines;
|
|
3224
|
-
const isActive = activePanelKey === m.id;
|
|
3225
|
-
const card = document.createElement('div');
|
|
3349
|
+
function renderModuleGrid(c) {
|
|
3350
|
+
const q = searchQuery.toLowerCase();
|
|
3351
|
+
const list = D.modules.filter(m => {
|
|
3352
|
+
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
3353
|
+
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
3354
|
+
return true;
|
|
3355
|
+
});
|
|
3356
|
+
const typeKey = [...activeTypes].sort().join(',');
|
|
3357
|
+
const rowsRenderKey = `modules:${contextMode}:${view}:${typeKey}:${q}`;
|
|
3358
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(list, rowsRenderKey);
|
|
3359
|
+
|
|
3360
|
+
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
3361
|
+
|
|
3362
|
+
c.innerHTML = `
|
|
3363
|
+
<div class="module-grid" id="modGrid"></div>
|
|
3364
|
+
${hasMoreRows ? `
|
|
3365
|
+
<div style="margin-top:10px;display:flex;align-items:center;justify-content:space-between;gap:8px;padding:10px;border:1px solid var(--border);border-radius:8px;background:var(--bg-card);font-size:12px;color:var(--dim)">
|
|
3366
|
+
<span>Показано ${visibleRows.length} из ${list.length} модулей</span>
|
|
3367
|
+
<button class="bulk-actions-btn" id="moduleRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3368
|
+
</div>` : ''}
|
|
3369
|
+
`;
|
|
3370
|
+
const grid = document.getElementById('modGrid');
|
|
3371
|
+
|
|
3372
|
+
visibleRows.forEach(m => {
|
|
3373
|
+
const cov = m.coverage?.lines;
|
|
3374
|
+
const isActive = activePanelKey === m.id;
|
|
3375
|
+
const card = document.createElement('div');
|
|
3226
3376
|
card.className = 'module-card' + (isActive ? ' active' : '');
|
|
3227
3377
|
card.innerHTML = `
|
|
3228
3378
|
<div class="module-name">${m.name}</div>
|
|
@@ -3234,18 +3384,18 @@ function renderModuleGrid(c) {
|
|
|
3234
3384
|
<span class="badge ${m.hasTests ? 'badge-green' : 'badge-red'}">${m.hasTests ? '✓' : '✗'}</span>
|
|
3235
3385
|
</div>
|
|
3236
3386
|
${cov != null ? `<div class="cov-bar"><div class="cov-fill" style="width:${cov}%;background:${covColor(cov)}"></div></div>` : ''}`;
|
|
3237
|
-
card.onclick = () => openModulePanel(m);
|
|
3238
|
-
grid.appendChild(card);
|
|
3239
|
-
});
|
|
3240
|
-
|
|
3241
|
-
const moduleRowsLoadMoreBtn = document.getElementById('moduleRowsLoadMoreBtn');
|
|
3242
|
-
if (moduleRowsLoadMoreBtn) {
|
|
3243
|
-
moduleRowsLoadMoreBtn.onclick = () => {
|
|
3244
|
-
increaseFileRowsLimit();
|
|
3245
|
-
renderContent();
|
|
3246
|
-
};
|
|
3247
|
-
}
|
|
3248
|
-
}
|
|
3387
|
+
card.onclick = () => openModulePanel(m);
|
|
3388
|
+
grid.appendChild(card);
|
|
3389
|
+
});
|
|
3390
|
+
|
|
3391
|
+
const moduleRowsLoadMoreBtn = document.getElementById('moduleRowsLoadMoreBtn');
|
|
3392
|
+
if (moduleRowsLoadMoreBtn) {
|
|
3393
|
+
moduleRowsLoadMoreBtn.onclick = () => {
|
|
3394
|
+
increaseFileRowsLimit();
|
|
3395
|
+
renderContent();
|
|
3396
|
+
};
|
|
3397
|
+
}
|
|
3398
|
+
}
|
|
3249
3399
|
|
|
3250
3400
|
// ─── Panels ───────────────────────────────────────────────────────────────────
|
|
3251
3401
|
function openFeaturePanel(key) {
|
|
@@ -3456,29 +3606,29 @@ function openUnmappedPanel(files, infraFiles) {
|
|
|
3456
3606
|
document.getElementById('panel').classList.add('open');
|
|
3457
3607
|
}
|
|
3458
3608
|
|
|
3459
|
-
function closePanel() {
|
|
3460
|
-
activePanelKey = null;
|
|
3461
|
-
document.getElementById('panel').classList.remove('open');
|
|
3462
|
-
renderContent();
|
|
3463
|
-
}
|
|
3464
|
-
|
|
3465
|
-
function applyTerminalFiltersFromUi() {
|
|
3466
|
-
terminalSearchQuery = document.getElementById('agentSearchInput')?.value || '';
|
|
3467
|
-
terminalSearchErrorsOnly = !!document.getElementById('agentSearchErrorsOnly')?.checked;
|
|
3468
|
-
terminalSearchCurrentRunOnly = !!document.getElementById('agentSearchCurrentRunOnly')?.checked;
|
|
3469
|
-
terminalSearchRegex = !!document.getElementById('agentSearchRegex')?.checked;
|
|
3470
|
-
renderActiveSession();
|
|
3471
|
-
}
|
|
3472
|
-
|
|
3473
|
-
['agentSearchInput', 'agentSearchErrorsOnly', 'agentSearchCurrentRunOnly', 'agentSearchRegex'].forEach((id) => {
|
|
3474
|
-
const el = document.getElementById(id);
|
|
3475
|
-
if (!el) return;
|
|
3476
|
-
const eventName = id === 'agentSearchInput' ? 'input' : 'change';
|
|
3477
|
-
el.addEventListener(eventName, applyTerminalFiltersFromUi);
|
|
3478
|
-
});
|
|
3479
|
-
|
|
3480
|
-
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
3481
|
-
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
3609
|
+
function closePanel() {
|
|
3610
|
+
activePanelKey = null;
|
|
3611
|
+
document.getElementById('panel').classList.remove('open');
|
|
3612
|
+
renderContent();
|
|
3613
|
+
}
|
|
3614
|
+
|
|
3615
|
+
function applyTerminalFiltersFromUi() {
|
|
3616
|
+
terminalSearchQuery = document.getElementById('agentSearchInput')?.value || '';
|
|
3617
|
+
terminalSearchErrorsOnly = !!document.getElementById('agentSearchErrorsOnly')?.checked;
|
|
3618
|
+
terminalSearchCurrentRunOnly = !!document.getElementById('agentSearchCurrentRunOnly')?.checked;
|
|
3619
|
+
terminalSearchRegex = !!document.getElementById('agentSearchRegex')?.checked;
|
|
3620
|
+
renderActiveSession();
|
|
3621
|
+
}
|
|
3622
|
+
|
|
3623
|
+
['agentSearchInput', 'agentSearchErrorsOnly', 'agentSearchCurrentRunOnly', 'agentSearchRegex'].forEach((id) => {
|
|
3624
|
+
const el = document.getElementById(id);
|
|
3625
|
+
if (!el) return;
|
|
3626
|
+
const eventName = id === 'agentSearchInput' ? 'input' : 'change';
|
|
3627
|
+
el.addEventListener(eventName, applyTerminalFiltersFromUi);
|
|
3628
|
+
});
|
|
3629
|
+
|
|
3630
|
+
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
3631
|
+
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
3482
3632
|
tab.onclick = () => {
|
|
3483
3633
|
if (contextMode !== 'qa') return;
|
|
3484
3634
|
if (tab.classList.contains('disabled')) return;
|
|
@@ -3528,199 +3678,199 @@ window.addEventListener('popstate', () => {
|
|
|
3528
3678
|
});
|
|
3529
3679
|
|
|
3530
3680
|
// ─── Live reload ──────────────────────────────────────────────────────────────
|
|
3531
|
-
function setLiveDot(color, title) {
|
|
3532
|
-
const dot = document.getElementById('liveDot');
|
|
3533
|
-
dot.style.background = color;
|
|
3534
|
-
dot.title = title;
|
|
3535
|
-
}
|
|
3536
|
-
|
|
3537
|
-
function scheduleRefreshData(delayMs = 120) {
|
|
3538
|
-
if (refreshDataTimer) return;
|
|
3539
|
-
refreshDataTimer = setTimeout(() => {
|
|
3540
|
-
refreshDataTimer = null;
|
|
3541
|
-
void refreshData();
|
|
3542
|
-
}, delayMs);
|
|
3543
|
-
}
|
|
3544
|
-
|
|
3545
|
-
async function refreshData() {
|
|
3546
|
-
if (refreshDataInFlight) {
|
|
3547
|
-
refreshDataQueued = true;
|
|
3548
|
-
return;
|
|
3549
|
-
}
|
|
3550
|
-
refreshDataInFlight = true;
|
|
3551
|
-
try {
|
|
3552
|
-
const res = await fetch('/api/data');
|
|
3553
|
-
D = await res.json();
|
|
3681
|
+
function setLiveDot(color, title) {
|
|
3682
|
+
const dot = document.getElementById('liveDot');
|
|
3683
|
+
dot.style.background = color;
|
|
3684
|
+
dot.title = title;
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
function scheduleRefreshData(delayMs = 120) {
|
|
3688
|
+
if (refreshDataTimer) return;
|
|
3689
|
+
refreshDataTimer = setTimeout(() => {
|
|
3690
|
+
refreshDataTimer = null;
|
|
3691
|
+
void refreshData();
|
|
3692
|
+
}, delayMs);
|
|
3693
|
+
}
|
|
3694
|
+
|
|
3695
|
+
async function refreshData() {
|
|
3696
|
+
if (refreshDataInFlight) {
|
|
3697
|
+
refreshDataQueued = true;
|
|
3698
|
+
return;
|
|
3699
|
+
}
|
|
3700
|
+
refreshDataInFlight = true;
|
|
3701
|
+
try {
|
|
3702
|
+
const res = await fetch('/api/data');
|
|
3703
|
+
D = await res.json();
|
|
3554
3704
|
|
|
3555
3705
|
// Update header timestamp
|
|
3556
3706
|
document.getElementById('scannedAt').textContent =
|
|
3557
3707
|
new Date(D.scannedAt).toLocaleTimeString();
|
|
3558
|
-
|
|
3559
|
-
renderStats();
|
|
3560
|
-
renderSidebar();
|
|
3561
|
-
updateAgentBtn();
|
|
3562
|
-
updateAgentRightsInfo();
|
|
3563
|
-
|
|
3564
|
-
// Re-render drill-down or re-open panel
|
|
3565
|
-
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
3708
|
+
|
|
3709
|
+
renderStats();
|
|
3710
|
+
renderSidebar();
|
|
3711
|
+
updateAgentBtn();
|
|
3712
|
+
updateAgentRightsInfo();
|
|
3713
|
+
|
|
3714
|
+
// Re-render drill-down or re-open panel
|
|
3715
|
+
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
3566
3716
|
if (panelOpen && activePanelKey) {
|
|
3567
3717
|
if (drillFeatureKey === '__unmapped__') {
|
|
3568
3718
|
renderContent(); // already routes to renderUnmappedDetail
|
|
3569
3719
|
} else if (view === 'features' && D.features) {
|
|
3570
3720
|
openFeaturePanel(activePanelKey);
|
|
3571
|
-
} else {
|
|
3572
|
-
const m = D.modules.find(m => m.id === activePanelKey);
|
|
3573
|
-
if (m) openModulePanel(m);
|
|
3574
|
-
else closePanel();
|
|
3575
|
-
}
|
|
3576
|
-
} else {
|
|
3577
|
-
renderContent();
|
|
3578
|
-
}
|
|
3579
|
-
|
|
3580
|
-
// Brief green flash on the dot to signal fresh data
|
|
3581
|
-
setLiveDot('#3fb950', 'Live — обновлено только что');
|
|
3582
|
-
setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
|
|
3583
|
-
void loadAgentState();
|
|
3584
|
-
} catch (err) {
|
|
3585
|
-
console.error('Refresh failed:', err);
|
|
3586
|
-
} finally {
|
|
3587
|
-
refreshDataInFlight = false;
|
|
3588
|
-
if (refreshDataQueued) {
|
|
3589
|
-
refreshDataQueued = false;
|
|
3590
|
-
scheduleRefreshData(120);
|
|
3591
|
-
}
|
|
3592
|
-
}
|
|
3593
|
-
}
|
|
3594
|
-
|
|
3595
|
-
function connectSSE() {
|
|
3596
|
-
const es = new EventSource('/api/events');
|
|
3597
|
-
|
|
3598
|
-
es.onopen = () => {
|
|
3599
|
-
setLiveDot('#3fb950', 'Live — автообновление включено');
|
|
3600
|
-
void loadAgentState();
|
|
3601
|
-
};
|
|
3602
|
-
|
|
3603
|
-
es.addEventListener('data-updated', () => {
|
|
3604
|
-
setLiveDot('#e3b341', 'Обновляю данные…');
|
|
3605
|
-
scheduleRefreshData();
|
|
3606
|
-
});
|
|
3607
|
-
|
|
3608
|
-
es.addEventListener('agent-queue-updated', (e) => {
|
|
3609
|
-
const payload = JSON.parse(e.data || '{}');
|
|
3610
|
-
agentQueueState = Array.isArray(payload.queue) ? payload.queue : [];
|
|
3611
|
-
refreshPathActivityFromState();
|
|
3612
|
-
updateQueueBadge(agentQueueState.length);
|
|
3613
|
-
renderContent();
|
|
3614
|
-
});
|
|
3615
|
-
|
|
3616
|
-
es.addEventListener('agent-run-created', (e) => {
|
|
3617
|
-
const { run } = JSON.parse(e.data || '{}');
|
|
3618
|
-
upsertRunState(run);
|
|
3619
|
-
});
|
|
3620
|
-
|
|
3621
|
-
es.addEventListener('agent-run-updated', (e) => {
|
|
3622
|
-
const { run } = JSON.parse(e.data || '{}');
|
|
3623
|
-
if (!run) return;
|
|
3624
|
-
if (run.runId && agentQueueState.length > 0) {
|
|
3625
|
-
agentQueueState = agentQueueState.filter((q) => q.runId !== run.runId || run.phase === 'queued');
|
|
3626
|
-
updateQueueBadge(agentQueueState.length);
|
|
3627
|
-
}
|
|
3628
|
-
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
3629
|
-
agentActiveRun = run;
|
|
3630
|
-
const startedPaths = getTaskFilePaths(run.task, run.featureKey, run.filePath, run.selectedFilePaths);
|
|
3631
|
-
startedPaths.forEach((p) => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3632
|
-
renderContent();
|
|
3633
|
-
}
|
|
3634
|
-
updateSessionFromRun(run);
|
|
3635
|
-
});
|
|
3636
|
-
|
|
3637
|
-
es.addEventListener('agent-run-finished', (e) => {
|
|
3638
|
-
const { run } = JSON.parse(e.data || '{}');
|
|
3639
|
-
if (!run) return;
|
|
3640
|
-
updateSessionFromRun(run);
|
|
3641
|
-
refreshPathActivityFromState();
|
|
3642
|
-
renderContent();
|
|
3643
|
-
});
|
|
3644
|
-
|
|
3645
|
-
es.addEventListener('agent-queued', (e) => {
|
|
3646
|
-
const { runId, queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3647
|
-
updateQueueBadge(queueLength);
|
|
3648
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
3649
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
3650
|
-
// Track queued paths for spinner
|
|
3651
|
-
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
3652
|
-
renderContent();
|
|
3653
|
-
// Append queue notification to the currently running session (or active)
|
|
3654
|
-
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3655
|
-
if (targetId) {
|
|
3656
|
-
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
3657
|
-
}
|
|
3658
|
-
void loadAgentState();
|
|
3659
|
-
});
|
|
3660
|
-
|
|
3661
|
-
es.addEventListener('agent-started', (e) => {
|
|
3662
|
-
setAgentRunning(true);
|
|
3663
|
-
const { runId, title, queueLength = 0, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3664
|
-
updateQueueBadge(queueLength);
|
|
3665
|
-
currentRunId = runId || null;
|
|
3666
|
-
// Move paths from queued → running (current task)
|
|
3667
|
-
const startedPaths = getTaskFilePaths(task, featureKey, filePath, selectedFilePaths);
|
|
3668
|
-
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3669
|
-
renderContent();
|
|
3670
|
-
// Close previous session (queue case: agent-done not fired between tasks)
|
|
3671
|
-
if (runningSessionId) {
|
|
3672
|
-
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
3673
|
-
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
3674
|
-
}
|
|
3675
|
-
const id = runId ? ensureSessionForRun({ runId, title, phase: 'running' }, true) : createSession(title);
|
|
3676
|
-
runningSessionId = id;
|
|
3677
|
-
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
3678
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
3679
|
-
});
|
|
3680
|
-
|
|
3681
|
-
es.addEventListener('agent-output', (e) => {
|
|
3682
|
-
const { runId, line, isError, isDim } = JSON.parse(e.data);
|
|
3683
|
-
let targetId = runningSessionId;
|
|
3684
|
-
if (runId) {
|
|
3685
|
-
targetId = runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'running' });
|
|
3686
|
-
if (targetId) runningSessionId = targetId;
|
|
3687
|
-
}
|
|
3688
|
-
if (targetId) {
|
|
3689
|
-
appendToSession(targetId, line, !!isError, !!isDim);
|
|
3690
|
-
if (activeSessionId === targetId) {
|
|
3691
|
-
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
3692
|
-
}
|
|
3693
|
-
} else {
|
|
3694
|
-
appendTerminalLine(line, !!isError, !!isDim);
|
|
3695
|
-
}
|
|
3721
|
+
} else {
|
|
3722
|
+
const m = D.modules.find(m => m.id === activePanelKey);
|
|
3723
|
+
if (m) openModulePanel(m);
|
|
3724
|
+
else closePanel();
|
|
3725
|
+
}
|
|
3726
|
+
} else {
|
|
3727
|
+
renderContent();
|
|
3728
|
+
}
|
|
3729
|
+
|
|
3730
|
+
// Brief green flash on the dot to signal fresh data
|
|
3731
|
+
setLiveDot('#3fb950', 'Live — обновлено только что');
|
|
3732
|
+
setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
|
|
3733
|
+
void loadAgentState();
|
|
3734
|
+
} catch (err) {
|
|
3735
|
+
console.error('Refresh failed:', err);
|
|
3736
|
+
} finally {
|
|
3737
|
+
refreshDataInFlight = false;
|
|
3738
|
+
if (refreshDataQueued) {
|
|
3739
|
+
refreshDataQueued = false;
|
|
3740
|
+
scheduleRefreshData(120);
|
|
3741
|
+
}
|
|
3742
|
+
}
|
|
3743
|
+
}
|
|
3744
|
+
|
|
3745
|
+
function connectSSE() {
|
|
3746
|
+
const es = new EventSource('/api/events');
|
|
3747
|
+
|
|
3748
|
+
es.onopen = () => {
|
|
3749
|
+
setLiveDot('#3fb950', 'Live — автообновление включено');
|
|
3750
|
+
void loadAgentState();
|
|
3751
|
+
};
|
|
3752
|
+
|
|
3753
|
+
es.addEventListener('data-updated', () => {
|
|
3754
|
+
setLiveDot('#e3b341', 'Обновляю данные…');
|
|
3755
|
+
scheduleRefreshData();
|
|
3756
|
+
});
|
|
3757
|
+
|
|
3758
|
+
es.addEventListener('agent-queue-updated', (e) => {
|
|
3759
|
+
const payload = JSON.parse(e.data || '{}');
|
|
3760
|
+
agentQueueState = Array.isArray(payload.queue) ? payload.queue : [];
|
|
3761
|
+
refreshPathActivityFromState();
|
|
3762
|
+
updateQueueBadge(agentQueueState.length);
|
|
3763
|
+
renderContent();
|
|
3696
3764
|
});
|
|
3697
3765
|
|
|
3698
|
-
es.addEventListener('agent-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3766
|
+
es.addEventListener('agent-run-created', (e) => {
|
|
3767
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3768
|
+
upsertRunState(run);
|
|
3769
|
+
});
|
|
3770
|
+
|
|
3771
|
+
es.addEventListener('agent-run-updated', (e) => {
|
|
3772
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3773
|
+
if (!run) return;
|
|
3774
|
+
if (run.runId && agentQueueState.length > 0) {
|
|
3775
|
+
agentQueueState = agentQueueState.filter((q) => q.runId !== run.runId || run.phase === 'queued');
|
|
3776
|
+
updateQueueBadge(agentQueueState.length);
|
|
3777
|
+
}
|
|
3778
|
+
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
3779
|
+
agentActiveRun = run;
|
|
3780
|
+
const startedPaths = getTaskFilePaths(run.task, run.featureKey, run.filePath, run.selectedFilePaths);
|
|
3781
|
+
startedPaths.forEach((p) => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3782
|
+
renderContent();
|
|
3783
|
+
}
|
|
3784
|
+
updateSessionFromRun(run);
|
|
3785
|
+
});
|
|
3786
|
+
|
|
3787
|
+
es.addEventListener('agent-run-finished', (e) => {
|
|
3788
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3789
|
+
if (!run) return;
|
|
3790
|
+
updateSessionFromRun(run);
|
|
3791
|
+
refreshPathActivityFromState();
|
|
3792
|
+
renderContent();
|
|
3793
|
+
});
|
|
3794
|
+
|
|
3795
|
+
es.addEventListener('agent-queued', (e) => {
|
|
3796
|
+
const { runId, queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3797
|
+
updateQueueBadge(queueLength);
|
|
3798
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
3799
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
3800
|
+
// Track queued paths for spinner
|
|
3801
|
+
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
3802
|
+
renderContent();
|
|
3803
|
+
// Append queue notification to the currently running session (or active)
|
|
3804
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3805
|
+
if (targetId) {
|
|
3806
|
+
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
3807
|
+
}
|
|
3808
|
+
void loadAgentState();
|
|
3809
|
+
});
|
|
3810
|
+
|
|
3811
|
+
es.addEventListener('agent-started', (e) => {
|
|
3812
|
+
setAgentRunning(true);
|
|
3813
|
+
const { runId, title, queueLength = 0, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3814
|
+
updateQueueBadge(queueLength);
|
|
3815
|
+
currentRunId = runId || null;
|
|
3816
|
+
// Move paths from queued → running (current task)
|
|
3817
|
+
const startedPaths = getTaskFilePaths(task, featureKey, filePath, selectedFilePaths);
|
|
3818
|
+
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3819
|
+
renderContent();
|
|
3820
|
+
// Close previous session (queue case: agent-done not fired between tasks)
|
|
3821
|
+
if (runningSessionId) {
|
|
3822
|
+
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
3823
|
+
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
3824
|
+
}
|
|
3825
|
+
const id = runId ? ensureSessionForRun({ runId, title, phase: 'running' }, true) : createSession(title);
|
|
3826
|
+
runningSessionId = id;
|
|
3827
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
3828
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
3829
|
+
});
|
|
3830
|
+
|
|
3831
|
+
es.addEventListener('agent-output', (e) => {
|
|
3832
|
+
const { runId, line, isError, isDim } = JSON.parse(e.data);
|
|
3833
|
+
let targetId = runningSessionId;
|
|
3834
|
+
if (runId) {
|
|
3835
|
+
targetId = runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'running' });
|
|
3836
|
+
if (targetId) runningSessionId = targetId;
|
|
3837
|
+
}
|
|
3838
|
+
if (targetId) {
|
|
3839
|
+
appendToSession(targetId, line, !!isError, !!isDim);
|
|
3840
|
+
if (activeSessionId === targetId) {
|
|
3841
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
3842
|
+
}
|
|
3843
|
+
} else {
|
|
3844
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
3845
|
+
}
|
|
3846
|
+
});
|
|
3847
|
+
|
|
3848
|
+
es.addEventListener('agent-done', () => {
|
|
3849
|
+
setAgentRunning(false);
|
|
3850
|
+
if (agentQueueState.length === 0) updateQueueBadge(0);
|
|
3851
|
+
agentRunningPaths.clear();
|
|
3852
|
+
if (runningSessionId) {
|
|
3853
|
+
updateSessionStatus(runningSessionId, 'ok');
|
|
3854
|
+
if (activeSessionId === runningSessionId) {
|
|
3855
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
3856
|
+
}
|
|
3707
3857
|
runningSessionId = null;
|
|
3708
3858
|
}
|
|
3709
3859
|
renderContent();
|
|
3710
3860
|
});
|
|
3711
3861
|
|
|
3712
|
-
es.addEventListener('agent-summary', (e) => {
|
|
3713
|
-
const {
|
|
3714
|
-
runId,
|
|
3715
|
-
validationStats = null,
|
|
3716
|
-
passed = 0, failed = 0, files = [],
|
|
3717
|
-
testedFileCount = files.length,
|
|
3718
|
-
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
3719
|
-
failedFileCount = failed > 0 ? 1 : 0,
|
|
3720
|
-
autoFixQueued = false,
|
|
3721
|
-
} = JSON.parse(e.data);
|
|
3722
|
-
const coverageOk = !validationStats || ((validationStats.notCovered || 0) === 0 && (validationStats.blocked || 0) === 0);
|
|
3723
|
-
const allOk = failed === 0 && coverageOk;
|
|
3862
|
+
es.addEventListener('agent-summary', (e) => {
|
|
3863
|
+
const {
|
|
3864
|
+
runId,
|
|
3865
|
+
validationStats = null,
|
|
3866
|
+
passed = 0, failed = 0, files = [],
|
|
3867
|
+
testedFileCount = files.length,
|
|
3868
|
+
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
3869
|
+
failedFileCount = failed > 0 ? 1 : 0,
|
|
3870
|
+
autoFixQueued = false,
|
|
3871
|
+
} = JSON.parse(e.data);
|
|
3872
|
+
const coverageOk = !validationStats || ((validationStats.notCovered || 0) === 0 && (validationStats.blocked || 0) === 0);
|
|
3873
|
+
const allOk = failed === 0 && coverageOk;
|
|
3724
3874
|
const box = document.createElement('div');
|
|
3725
3875
|
box.style.cssText = `
|
|
3726
3876
|
margin: 10px 0 4px;
|
|
@@ -3737,33 +3887,33 @@ function connectSSE() {
|
|
|
3737
3887
|
<div style="font-size:11px;color:var(--muted);margin-top:4px">
|
|
3738
3888
|
Файлы: ${testedFileCount} • passed: ${passedFileCount} • failed: ${failedFileCount}
|
|
3739
3889
|
</div>
|
|
3740
|
-
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3741
|
-
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3742
|
-
</div>
|
|
3743
|
-
${validationStats ? `<div style="font-size:11px;color:var(--muted);margin-top:2px">Coverage matrix: covered ${validationStats.covered || 0} • not-covered ${validationStats.notCovered || 0} • blocked ${validationStats.blocked || 0}</div>` : ''}
|
|
3744
|
-
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
3745
|
-
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
3746
|
-
`;
|
|
3747
|
-
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3748
|
-
if (targetId) {
|
|
3749
|
-
appendToSession(targetId, box);
|
|
3750
|
-
// Mark session status from test results immediately
|
|
3751
|
-
if (runningSessionId === targetId) {
|
|
3752
|
-
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
3890
|
+
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3891
|
+
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3892
|
+
</div>
|
|
3893
|
+
${validationStats ? `<div style="font-size:11px;color:var(--muted);margin-top:2px">Coverage matrix: covered ${validationStats.covered || 0} • not-covered ${validationStats.notCovered || 0} • blocked ${validationStats.blocked || 0}</div>` : ''}
|
|
3894
|
+
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
3895
|
+
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
3896
|
+
`;
|
|
3897
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3898
|
+
if (targetId) {
|
|
3899
|
+
appendToSession(targetId, box);
|
|
3900
|
+
// Mark session status from test results immediately
|
|
3901
|
+
if (runningSessionId === targetId) {
|
|
3902
|
+
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
3753
3903
|
}
|
|
3754
3904
|
}
|
|
3755
3905
|
});
|
|
3756
3906
|
|
|
3757
|
-
es.addEventListener('agent-error', (e) => {
|
|
3758
|
-
const { runId, message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
3759
|
-
if (!runId) setAgentRunning(false);
|
|
3760
|
-
agentRunningPaths.clear();
|
|
3761
|
-
agentQueuedPaths.clear();
|
|
3762
|
-
renderContent();
|
|
3763
|
-
|
|
3764
|
-
// Build error node (plain text or auth/install box)
|
|
3765
|
-
let node = null;
|
|
3766
|
-
if (authRequired || notInstalled) {
|
|
3907
|
+
es.addEventListener('agent-error', (e) => {
|
|
3908
|
+
const { runId, message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
3909
|
+
if (!runId) setAgentRunning(false);
|
|
3910
|
+
agentRunningPaths.clear();
|
|
3911
|
+
agentQueuedPaths.clear();
|
|
3912
|
+
renderContent();
|
|
3913
|
+
|
|
3914
|
+
// Build error node (plain text or auth/install box)
|
|
3915
|
+
let node = null;
|
|
3916
|
+
if (authRequired || notInstalled) {
|
|
3767
3917
|
node = document.createElement('div');
|
|
3768
3918
|
node.style.cssText = 'margin:10px 0 4px;padding:10px 14px;border-radius:8px;border:1px solid var(--red);background:#2a0d0d;font-family:inherit;';
|
|
3769
3919
|
node.innerHTML = `
|
|
@@ -3771,14 +3921,14 @@ function connectSSE() {
|
|
|
3771
3921
|
${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>` : ''}
|
|
3772
3922
|
${notInstalled ? `<div style="margin-top:6px;font-size:11px;color:var(--dim)">После установки перезапусти viberadar</div>` : ''}
|
|
3773
3923
|
`;
|
|
3774
|
-
}
|
|
3775
|
-
|
|
3776
|
-
// If no session exists yet (startup check fires before any run), create one
|
|
3777
|
-
const targetId = (runId ? (runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'failed' })) : runningSessionId) || (() => {
|
|
3778
|
-
if (authRequired || notInstalled) {
|
|
3779
|
-
const id = createSession('⚠️ Проверка агента', 'error');
|
|
3780
|
-
return id;
|
|
3781
|
-
}
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3926
|
+
// If no session exists yet (startup check fires before any run), create one
|
|
3927
|
+
const targetId = (runId ? (runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'failed' })) : runningSessionId) || (() => {
|
|
3928
|
+
if (authRequired || notInstalled) {
|
|
3929
|
+
const id = createSession('⚠️ Проверка агента', 'error');
|
|
3930
|
+
return id;
|
|
3931
|
+
}
|
|
3782
3932
|
return activeSessionId;
|
|
3783
3933
|
})();
|
|
3784
3934
|
|
|
@@ -3789,12 +3939,12 @@ function connectSSE() {
|
|
|
3789
3939
|
appendToSession(targetId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
3790
3940
|
}
|
|
3791
3941
|
updateSessionStatus(targetId, 'error');
|
|
3792
|
-
if (activeSessionId === targetId) {
|
|
3793
|
-
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
3796
|
-
if (!runId && runningSessionId) runningSessionId = null;
|
|
3797
|
-
});
|
|
3942
|
+
if (activeSessionId === targetId) {
|
|
3943
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3944
|
+
}
|
|
3945
|
+
}
|
|
3946
|
+
if (!runId && runningSessionId) runningSessionId = null;
|
|
3947
|
+
});
|
|
3798
3948
|
|
|
3799
3949
|
es.addEventListener('tests-started', (e) => {
|
|
3800
3950
|
const { testType, count } = JSON.parse(e.data);
|
|
@@ -3862,24 +4012,24 @@ function connectSSE() {
|
|
|
3862
4012
|
}
|
|
3863
4013
|
});
|
|
3864
4014
|
|
|
3865
|
-
es.onerror = () => {
|
|
3866
|
-
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
3867
|
-
es.close();
|
|
3868
|
-
setTimeout(connectSSE, 3000);
|
|
3869
|
-
};
|
|
3870
|
-
}
|
|
3871
|
-
|
|
3872
|
-
init().then(async () => {
|
|
3873
|
-
restoreSessions();
|
|
3874
|
-
await loadAgentState();
|
|
3875
|
-
connectSSE();
|
|
3876
|
-
applyTerminalFiltersFromUi();
|
|
3877
|
-
updateToolbarButtonsState();
|
|
3878
|
-
// Sync content padding with terminal panel open state automatically
|
|
3879
|
-
new MutationObserver(() => {
|
|
3880
|
-
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
3881
|
-
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
3882
|
-
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
4015
|
+
es.onerror = () => {
|
|
4016
|
+
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
4017
|
+
es.close();
|
|
4018
|
+
setTimeout(connectSSE, 3000);
|
|
4019
|
+
};
|
|
4020
|
+
}
|
|
4021
|
+
|
|
4022
|
+
init().then(async () => {
|
|
4023
|
+
restoreSessions();
|
|
4024
|
+
await loadAgentState();
|
|
4025
|
+
connectSSE();
|
|
4026
|
+
applyTerminalFiltersFromUi();
|
|
4027
|
+
updateToolbarButtonsState();
|
|
4028
|
+
// Sync content padding with terminal panel open state automatically
|
|
4029
|
+
new MutationObserver(() => {
|
|
4030
|
+
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
4031
|
+
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
4032
|
+
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
3883
4033
|
});
|
|
3884
4034
|
</script>
|
|
3885
4035
|
</body>
|