viberadar 0.3.64 → 0.3.65
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/ui/dashboard.html +1385 -1278
- 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.6fr .5fr .7fr .5fr 1.4fr .8fr; gap:8px; 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; }
|
|
@@ -675,12 +675,12 @@
|
|
|
675
675
|
.file-agent-spinner.running { border: 2px solid var(--yellow); border-top-color: transparent; animation: spin 0.7s linear infinite; }
|
|
676
676
|
.file-agent-spinner.queued { border: 2px solid var(--dim); border-top-color: transparent; animation: spin 1.5s linear infinite; }
|
|
677
677
|
@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
|
-
}
|
|
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
|
+
}
|
|
684
684
|
.err-item { display: flex; flex-direction: column; gap: 1px; }
|
|
685
685
|
.err-name { font-size: 11px; color: var(--muted); }
|
|
686
686
|
.err-msg { font-size: 10px; color: var(--red); font-family: monospace; opacity: 0.85; }
|
|
@@ -769,92 +769,92 @@
|
|
|
769
769
|
cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
770
770
|
}
|
|
771
771
|
.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); }
|
|
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); }
|
|
858
858
|
/* ── Console Tabs ───────────────────────────────────────────────────────── */
|
|
859
859
|
.agent-tabs-bar {
|
|
860
860
|
display: flex; align-items: stretch; overflow-x: auto;
|
|
@@ -934,50 +934,50 @@
|
|
|
934
934
|
border-color: var(--accent);
|
|
935
935
|
color: var(--accent);
|
|
936
936
|
}
|
|
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;
|
|
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;
|
|
971
971
|
font-size: 12px;
|
|
972
972
|
line-height: 1.5;
|
|
973
973
|
}
|
|
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 ────────────────────────────────────────────────────────────────── */
|
|
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 ────────────────────────────────────────────────────────────────── */
|
|
981
981
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
982
982
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
983
983
|
/* ─── E2E Plan UI ─────────────────────────────────────────────────── */
|
|
@@ -1089,34 +1089,34 @@
|
|
|
1089
1089
|
<div id="panelContent"></div>
|
|
1090
1090
|
</div>
|
|
1091
1091
|
|
|
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>
|
|
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>
|
|
1096
1096
|
<span class="agent-queue-badge" id="agentQueueBadge" style="display:none">📋 <span id="agentQueueCount">0</span> в очереди</span>
|
|
1097
1097
|
<button class="agent-panel-cancel" id="agentQueueClearBtn" onclick="clearAgentQueue()" title="Очистить очередь" style="display:none">🗑 очередь</button>
|
|
1098
1098
|
<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>
|
|
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>
|
|
1120
1120
|
|
|
1121
1121
|
<script>
|
|
1122
1122
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
@@ -1125,21 +1125,21 @@ let contextMode = 'qa';
|
|
|
1125
1125
|
let view = 'features';
|
|
1126
1126
|
let searchQuery = '';
|
|
1127
1127
|
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
|
-
};
|
|
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
|
+
};
|
|
1143
1143
|
|
|
1144
1144
|
function getModeFromPath(pathname = window.location.pathname) {
|
|
1145
1145
|
if (pathname.startsWith('/radar/observability')) return 'observability';
|
|
@@ -1160,31 +1160,31 @@ function saveModeState(mode) {
|
|
|
1160
1160
|
modeStore[mode] = {
|
|
1161
1161
|
view,
|
|
1162
1162
|
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];
|
|
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
1173
|
view = state.view;
|
|
1174
1174
|
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
|
-
}
|
|
1175
|
+
activeTypes = new Set(state.activeTypes);
|
|
1176
|
+
drillFeatureKey = state.drillFeatureKey;
|
|
1177
|
+
drillTestType = state.drillTestType;
|
|
1178
|
+
activePanelKey = state.activePanelKey;
|
|
1179
|
+
showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
|
|
1180
|
+
}
|
|
1181
1181
|
|
|
1182
1182
|
function switchMode(nextMode) {
|
|
1183
1183
|
if (nextMode === contextMode) return;
|
|
1184
1184
|
saveModeState(contextMode);
|
|
1185
1185
|
contextMode = nextMode;
|
|
1186
1186
|
restoreModeState(contextMode);
|
|
1187
|
-
if (contextMode === 'observability') {
|
|
1187
|
+
if (contextMode === 'observability') {
|
|
1188
1188
|
view = 'files';
|
|
1189
1189
|
drillFeatureKey = null;
|
|
1190
1190
|
drillTestType = null;
|
|
@@ -1198,11 +1198,11 @@ function switchMode(nextMode) {
|
|
|
1198
1198
|
renderSidebar();
|
|
1199
1199
|
renderContent();
|
|
1200
1200
|
}
|
|
1201
|
-
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1202
|
-
let runAllRunning = false;
|
|
1203
|
-
let refreshDataInFlight = false;
|
|
1204
|
-
let refreshDataQueued = false;
|
|
1205
|
-
let refreshDataTimer = null;
|
|
1201
|
+
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1202
|
+
let runAllRunning = false;
|
|
1203
|
+
let refreshDataInFlight = false;
|
|
1204
|
+
let refreshDataQueued = false;
|
|
1205
|
+
let refreshDataTimer = null;
|
|
1206
1206
|
|
|
1207
1207
|
function escapeHtml(text) {
|
|
1208
1208
|
return String(text || '')
|
|
@@ -1241,42 +1241,42 @@ function setFeatureDrill(featureKey, syncHash = true) {
|
|
|
1241
1241
|
renderContent();
|
|
1242
1242
|
}
|
|
1243
1243
|
|
|
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) => {
|
|
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) => {
|
|
1247
1247
|
if (visibleSet && !visibleSet.has(relPath)) return false;
|
|
1248
1248
|
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() {
|
|
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() {
|
|
1280
1280
|
const hash = (window.location.hash || '').replace(/^#/, '');
|
|
1281
1281
|
if (!hash) {
|
|
1282
1282
|
if (view === 'features' && drillFeatureKey) {
|
|
@@ -1324,145 +1324,145 @@ async function runAllTests() {
|
|
|
1324
1324
|
}
|
|
1325
1325
|
|
|
1326
1326
|
// ─── 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;
|
|
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;
|
|
1354
1354
|
|
|
1355
1355
|
function _sessionId() {
|
|
1356
1356
|
return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
1357
1357
|
}
|
|
1358
1358
|
|
|
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
|
-
}
|
|
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
1369
|
|
|
1370
1370
|
function restoreSessions() {
|
|
1371
1371
|
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();
|
|
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();
|
|
1386
1386
|
}
|
|
1387
1387
|
} catch {}
|
|
1388
1388
|
}
|
|
1389
1389
|
|
|
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();
|
|
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();
|
|
1411
1411
|
return s.id;
|
|
1412
1412
|
}
|
|
1413
1413
|
|
|
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' ? '❌ ошибка'
|
|
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' ? '❌ ошибка'
|
|
1422
1422
|
: '';
|
|
1423
1423
|
document.getElementById('agentPanelTitle').textContent = s.title;
|
|
1424
1424
|
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;
|
|
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;
|
|
1452
1452
|
let stored;
|
|
1453
1453
|
if (typeof lineOrNode === 'string') {
|
|
1454
1454
|
stored = { text: lineOrNode, isError, isDim };
|
|
1455
1455
|
} else {
|
|
1456
1456
|
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
|
-
}
|
|
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
|
+
}
|
|
1466
1466
|
|
|
1467
1467
|
function updateSessionStatus(id, status) {
|
|
1468
1468
|
const s = consoleSessions.find(s => s.id === id);
|
|
@@ -1490,9 +1490,9 @@ function renderTabs() {
|
|
|
1490
1490
|
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1491
1491
|
}
|
|
1492
1492
|
|
|
1493
|
-
function copyTerminalContent() {
|
|
1494
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1495
|
-
if (!s || s.lines.length === 0) return;
|
|
1493
|
+
function copyTerminalContent() {
|
|
1494
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1495
|
+
if (!s || s.lines.length === 0) return;
|
|
1496
1496
|
// Extract plain text from each line (strip HTML for rich nodes)
|
|
1497
1497
|
const text = s.lines.map(l => {
|
|
1498
1498
|
if (l.text !== undefined) return l.text;
|
|
@@ -1510,214 +1510,214 @@ function copyTerminalContent() {
|
|
|
1510
1510
|
btn.textContent = '✓';
|
|
1511
1511
|
btn.classList.add('copied');
|
|
1512
1512
|
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
|
-
}
|
|
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
|
+
}
|
|
1721
1721
|
|
|
1722
1722
|
async function setAgent(agent) {
|
|
1723
1723
|
await fetch('/api/set-agent', {
|
|
@@ -1766,157 +1766,157 @@ function isFileAgentActive(relPath) {
|
|
|
1766
1766
|
return null;
|
|
1767
1767
|
}
|
|
1768
1768
|
|
|
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);
|
|
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);
|
|
1920
1920
|
updateSessionStatus(runningSessionId, 'error');
|
|
1921
1921
|
runningSessionId = null;
|
|
1922
1922
|
} else {
|
|
@@ -1924,11 +1924,11 @@ async function cancelAgent() {
|
|
|
1924
1924
|
}
|
|
1925
1925
|
}
|
|
1926
1926
|
|
|
1927
|
-
async function clearAgentQueue() {
|
|
1928
|
-
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1929
|
-
await loadAgentState();
|
|
1930
|
-
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1931
|
-
}
|
|
1927
|
+
async function clearAgentQueue() {
|
|
1928
|
+
await fetch('/api/clear-queue', { method: 'POST' });
|
|
1929
|
+
await loadAgentState();
|
|
1930
|
+
appendTerminalLine('🗑 Очередь очищена', false);
|
|
1931
|
+
}
|
|
1932
1932
|
|
|
1933
1933
|
// ─── File row more menu ──────────────────────────────────────────────────────
|
|
1934
1934
|
let _openFileMenu = null;
|
|
@@ -2195,19 +2195,29 @@ function renderStats() {
|
|
|
2195
2195
|
|
|
2196
2196
|
let items;
|
|
2197
2197
|
if (contextMode === 'observability') {
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2198
|
+
const o = D.observability;
|
|
2199
|
+
if (o) {
|
|
2200
|
+
const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
|
|
2201
|
+
const structPct = Math.round(o.metrics.structured_completeness * 100);
|
|
2202
|
+
const missingCoverage = o.missingCriticalLogs.length;
|
|
2203
|
+
const noisyCount = o.topNoisyPatterns.length;
|
|
2204
|
+
items = [
|
|
2205
|
+
{ v: o.catalog.length, l: 'Источники логов' },
|
|
2206
|
+
{ v: noiseRatio + '%', l: 'Коэффициент шума', c: noiseRatio > 30 ? '#f85149' : noiseRatio > 10 ? '#e3b341' : undefined },
|
|
2207
|
+
{ v: structPct + '%', l: 'Структурированность', c: structPct < 50 ? '#f85149' : structPct < 80 ? '#e3b341' : undefined },
|
|
2208
|
+
{ v: missingCoverage, l: 'Нет покрытия', c: missingCoverage ? '#e3b341' : undefined },
|
|
2209
|
+
{ v: noisyCount, l: 'Шумных паттернов', c: noisyCount ? '#e3b341' : undefined },
|
|
2210
|
+
];
|
|
2211
|
+
} else {
|
|
2212
|
+
const serviceSources = src.filter(m => m.type === 'service' || m.type === 'util' || m.type === 'other');
|
|
2213
|
+
items = [
|
|
2214
|
+
{ v: serviceSources.length, l: 'Источники логов' },
|
|
2215
|
+
{ v: '—', l: 'Коэффициент шума' },
|
|
2216
|
+
{ v: '—', l: 'Структурированность' },
|
|
2217
|
+
{ v: '—', l: 'Нет покрытия' },
|
|
2218
|
+
{ v: '—', l: 'Шумных паттернов' },
|
|
2219
|
+
];
|
|
2220
|
+
}
|
|
2211
2221
|
} else if (D.hasConfig && D.features) {
|
|
2212
2222
|
const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
|
|
2213
2223
|
items = [
|
|
@@ -2236,10 +2246,10 @@ function renderStats() {
|
|
|
2236
2246
|
|
|
2237
2247
|
function renderModeSwitch() {
|
|
2238
2248
|
const root = document.getElementById('modeSwitch');
|
|
2239
|
-
const modes = [
|
|
2240
|
-
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2241
|
-
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2242
|
-
];
|
|
2249
|
+
const modes = [
|
|
2250
|
+
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2251
|
+
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2252
|
+
];
|
|
2243
2253
|
root.innerHTML = modes.map(m => `
|
|
2244
2254
|
<button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
|
|
2245
2255
|
<span>${m.label}</span>
|
|
@@ -2257,15 +2267,15 @@ function renderSidebar() {
|
|
|
2257
2267
|
const tabs = document.getElementById('viewTabs');
|
|
2258
2268
|
const extra = document.getElementById('sidebarExtra');
|
|
2259
2269
|
|
|
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
|
-
}
|
|
2270
|
+
if (contextMode === 'observability') {
|
|
2271
|
+
tabs.style.display = 'none';
|
|
2272
|
+
extra.innerHTML = `
|
|
2273
|
+
<div class="sidebar-label">Фокус наблюдаемости</div>
|
|
2274
|
+
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
|
|
2275
|
+
Источники логов и качество сигналов для триажа инцидентов.
|
|
2276
|
+
</div>`;
|
|
2277
|
+
return;
|
|
2278
|
+
}
|
|
2269
2279
|
|
|
2270
2280
|
tabs.style.display = 'flex';
|
|
2271
2281
|
document.querySelectorAll('.view-tab').forEach(t =>
|
|
@@ -2325,61 +2335,158 @@ function renderQaOnboarding() {
|
|
|
2325
2335
|
return `<div class="onboarding-block"><h3>QA Coverage: что это?</h3><p>Этот экран помогает найти пробелы в тестах: сначала проверь покрытие по фичам, затем открой критичные непокрытые файлы и запусти генерацию/фиксы тестов.</p></div>`;
|
|
2326
2336
|
}
|
|
2327
2337
|
|
|
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
|
-
|
|
2338
|
+
function renderObservability(c) {
|
|
2339
|
+
const o = D.observability;
|
|
2340
|
+
if (!o) {
|
|
2341
|
+
c.innerHTML = `<div class="onboarding-block"><h3>Наблюдаемость</h3><p>Данные анализа логов недоступны. Пересканируй проект.</p></div>`;
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
|
|
2346
|
+
const structPct = Math.round(o.metrics.structured_completeness * 100);
|
|
2347
|
+
const actionPct = Math.round(o.metrics.error_actionability * 100);
|
|
2348
|
+
const coveragePct = Math.round(o.metrics.coverage_of_key_flows * 100);
|
|
2349
|
+
|
|
2350
|
+
function metricColor(val, goodThreshold, warnThreshold, invert) {
|
|
2351
|
+
// invert=true: higher is worse (noise), invert=false: higher is better (coverage)
|
|
2352
|
+
if (invert) return val > warnThreshold ? 'var(--red)' : val > goodThreshold ? 'var(--yellow)' : 'var(--green)';
|
|
2353
|
+
return val < warnThreshold ? 'var(--red)' : val < goodThreshold ? 'var(--yellow)' : 'var(--green)';
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2356
|
+
const formatLabels = { structured: 'structured', mixed: 'mixed', unstructured: 'unstructured' };
|
|
2357
|
+
const sourceByFormat = [
|
|
2358
|
+
{ label: 'Structured', count: o.catalog.filter(c => c.format === 'structured').length, color: 'var(--green)' },
|
|
2359
|
+
{ label: 'Mixed', count: o.catalog.filter(c => c.format === 'mixed').length, color: 'var(--yellow)' },
|
|
2360
|
+
{ label: 'Unstructured', count: o.catalog.filter(c => c.format === 'unstructured').length, color: 'var(--red)' },
|
|
2361
|
+
].filter(x => x.count > 0);
|
|
2362
|
+
|
|
2363
|
+
const recLabels = {
|
|
2364
|
+
'suppress': 'убрать',
|
|
2365
|
+
'downgrade level': 'понизить уровень',
|
|
2366
|
+
'enrich fields': 'обогатить поля',
|
|
2367
|
+
'add event': 'добавить событие',
|
|
2368
|
+
};
|
|
2369
|
+
|
|
2370
|
+
const noisyRows = o.topNoisyPatterns.slice(0, 8).map(i => `
|
|
2371
|
+
<div class="obs-list-item">
|
|
2372
|
+
<span class="obs-priority-${i.priority}" style="flex-shrink:0">[${i.priority}]</span>
|
|
2373
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(i.pattern)}">${escapeHtml(i.pattern)}</span>
|
|
2374
|
+
<strong style="flex-shrink:0;color:var(--red)">×${i.count} → ${recLabels[i.recommendation] || i.recommendation}</strong>
|
|
2375
|
+
</div>`).join('') || '<div class="obs-sub">Шумных паттернов не обнаружено</div>';
|
|
2376
|
+
|
|
2377
|
+
const missingRows = o.missingCriticalLogs.slice(0, 8).map(i => `
|
|
2378
|
+
<div class="obs-list-item">
|
|
2379
|
+
<span class="obs-priority-${i.priority}" style="flex-shrink:0">[${i.priority}]</span>
|
|
2380
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(i.pattern)}">${escapeHtml(i.pattern)}</span>
|
|
2381
|
+
<strong style="flex-shrink:0;color:var(--yellow)">${recLabels[i.recommendation] || i.recommendation}</strong>
|
|
2382
|
+
</div>`).join('') || '<div class="obs-sub" style="color:var(--green)">Критичные сценарии покрыты</div>';
|
|
2383
|
+
|
|
2384
|
+
const fieldGaps = o.fieldGaps || {};
|
|
2385
|
+
const fieldGapEntries = Object.entries(fieldGaps).filter(([,v]) => v > 0).sort((a,b) => b[1] - a[1]);
|
|
2386
|
+
const fieldGapRows = fieldGapEntries.map(([name, count]) => `
|
|
2387
|
+
<div class="obs-list-item">
|
|
2388
|
+
<span><code>${escapeHtml(name)}</code></span>
|
|
2389
|
+
<strong style="color:${count > 20 ? 'var(--red)' : count > 5 ? 'var(--yellow)' : 'var(--muted)'}">${count} пропусков</strong>
|
|
2390
|
+
</div>`).join('') || '<div class="obs-sub" style="color:var(--green)">Все обязательные поля на месте</div>';
|
|
2391
|
+
|
|
2392
|
+
const catalogRows = o.catalog.slice(0, 15).map(i => {
|
|
2393
|
+
const missing = (i.missingFields || []);
|
|
2394
|
+
const missingStr = missing.length ? missing.join(', ') : '—';
|
|
2395
|
+
return `
|
|
2396
|
+
<div class="obs-cat-row">
|
|
2397
|
+
<span title="${escapeHtml(i.modulePath)}">${escapeHtml(i.modulePath.split('/').slice(-2).join('/'))}</span>
|
|
2398
|
+
<span>${i.level}</span>
|
|
2399
|
+
<span style="color:${i.format==='structured'?'var(--green)':i.format==='mixed'?'var(--yellow)':'var(--red)'}">${i.format}</span>
|
|
2400
|
+
<span style="color:${missing.length > 4 ? 'var(--red)' : missing.length > 0 ? 'var(--yellow)' : 'var(--green)'}">${missing.length}/${missing.length > 0 ? '8' : '8'}</span>
|
|
2401
|
+
<span title="${escapeHtml(missingStr)}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(missingStr)}</span>
|
|
2402
|
+
<span style="color:${i.recommendation==='suppress'?'var(--red)':i.recommendation==='add event'?'var(--yellow)':'var(--muted)'}">${recLabels[i.recommendation] || i.recommendation}</span>
|
|
2403
|
+
</div>`}).join('');
|
|
2404
|
+
|
|
2405
|
+
c.innerHTML = `
|
|
2406
|
+
<div class="onboarding-block">
|
|
2407
|
+
<h3>Наблюдаемость: что это?</h3>
|
|
2408
|
+
<p>Аудит покрытия логами: что добавить, что убрать, что обогатить — на основе статического анализа лог-вызовов.</p>
|
|
2409
|
+
</div>
|
|
2410
|
+
|
|
2411
|
+
<div class="obs-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
|
|
2349
2412
|
<div class="obs-card">
|
|
2350
|
-
<div class="obs-title"
|
|
2413
|
+
<div class="obs-title">Коэффициент шума</div>
|
|
2414
|
+
<div class="obs-value" style="color:${metricColor(noiseRatio,10,30,true)}">${noiseRatio}%</div>
|
|
2415
|
+
<div class="obs-sub">Доля шумных лог-вызовов из всех.</div>
|
|
2416
|
+
</div>
|
|
2417
|
+
<div class="obs-card">
|
|
2418
|
+
<div class="obs-title">Структурированность</div>
|
|
2419
|
+
<div class="obs-value" style="color:${metricColor(structPct,80,50,false)}">${structPct}%</div>
|
|
2420
|
+
<div class="obs-sub">Логи с обязательными полями (module, event, traceId).</div>
|
|
2421
|
+
</div>
|
|
2422
|
+
<div class="obs-card">
|
|
2423
|
+
<div class="obs-title">Actionable ошибки</div>
|
|
2424
|
+
<div class="obs-value" style="color:${metricColor(actionPct,80,50,false)}">${actionPct}%</div>
|
|
2425
|
+
<div class="obs-sub">ERROR-логи с контекстом для диагностики.</div>
|
|
2426
|
+
</div>
|
|
2427
|
+
<div class="obs-card">
|
|
2428
|
+
<div class="obs-title">Покрытие сценариев</div>
|
|
2429
|
+
<div class="obs-value" style="color:${metricColor(coveragePct,80,50,false)}">${coveragePct}%</div>
|
|
2430
|
+
<div class="obs-sub">Модули с хотя бы одним warn/error событием.</div>
|
|
2431
|
+
</div>
|
|
2432
|
+
</div>
|
|
2433
|
+
|
|
2434
|
+
<div class="obs-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:12px">
|
|
2435
|
+
<div class="obs-card">
|
|
2436
|
+
<div class="obs-title">Источники по формату</div>
|
|
2437
|
+
<div class="obs-list">
|
|
2438
|
+
${sourceByFormat.map(g => `
|
|
2439
|
+
<div class="obs-list-item">
|
|
2440
|
+
<span style="color:${g.color}">${g.label}</span>
|
|
2441
|
+
<strong>${g.count}</strong>
|
|
2442
|
+
</div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
|
|
2443
|
+
</div>
|
|
2444
|
+
</div>
|
|
2445
|
+
<div class="obs-card">
|
|
2446
|
+
<div class="obs-title">Классификация логов</div>
|
|
2351
2447
|
<div class="obs-list">
|
|
2352
|
-
|
|
2448
|
+
<div class="obs-list-item"><span style="color:var(--red)">Мусор (убрать)</span><strong>${o.classification.trash}</strong></div>
|
|
2449
|
+
<div class="obs-list-item"><span style="color:var(--green)">Полезные</span><strong>${o.classification.useful}</strong></div>
|
|
2450
|
+
<div class="obs-list-item"><span style="color:var(--blue)">Критичные</span><strong>${o.classification.critical}</strong></div>
|
|
2353
2451
|
</div>
|
|
2354
2452
|
</div>
|
|
2453
|
+
<div class="obs-card">
|
|
2454
|
+
<div class="obs-title">Рекомендации</div>
|
|
2455
|
+
<div class="obs-list">
|
|
2456
|
+
${['suppress','enrich fields','add event','downgrade level'].map(rec => {
|
|
2457
|
+
const count = o.catalog.filter(c => c.recommendation === rec).length;
|
|
2458
|
+
if (!count) return '';
|
|
2459
|
+
return `<div class="obs-list-item"><span>${recLabels[rec]}</span><strong>${count}</strong></div>`;
|
|
2460
|
+
}).join('')}
|
|
2461
|
+
</div>
|
|
2462
|
+
</div>
|
|
2463
|
+
</div>
|
|
2464
|
+
|
|
2465
|
+
<div class="obs-grid" style="grid-template-columns:1fr 1fr;margin-bottom:12px">
|
|
2466
|
+
<div class="obs-card">
|
|
2467
|
+
<div class="obs-title">Что убрать — шумные паттерны</div>
|
|
2468
|
+
<div class="obs-list" style="gap:4px">${noisyRows}</div>
|
|
2469
|
+
</div>
|
|
2470
|
+
<div class="obs-card">
|
|
2471
|
+
<div class="obs-title">Что добавить — нет критичных логов</div>
|
|
2472
|
+
<div class="obs-list" style="gap:4px">${missingRows}</div>
|
|
2473
|
+
</div>
|
|
2474
|
+
</div>
|
|
2475
|
+
|
|
2476
|
+
<div class="obs-card" style="margin-bottom:12px">
|
|
2477
|
+
<div class="obs-title">Что обогатить — пробелы по полям</div>
|
|
2478
|
+
<div class="obs-sub" style="margin-bottom:8px">Обязательные поля по стандарту: service, env, trace_id, request_id, event_name, outcome, error_code (warn/error), user_id.</div>
|
|
2479
|
+
<div class="obs-list">${fieldGapRows}</div>
|
|
2480
|
+
</div>
|
|
2355
2481
|
|
|
2356
|
-
|
|
2357
|
-
|
|
2358
|
-
|
|
2359
|
-
|
|
2360
|
-
|
|
2361
|
-
</div>
|
|
2362
|
-
|
|
2363
|
-
|
|
2364
|
-
<div class="obs-title">Коэффициент шума</div>
|
|
2365
|
-
<div class="obs-value">${src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0}%</div>
|
|
2366
|
-
<div class="obs-sub">Доля источников без тестового контроля (прокси-метрика шумного логирования).</div>
|
|
2367
|
-
</div>
|
|
2368
|
-
|
|
2369
|
-
<div class="obs-card">
|
|
2370
|
-
<div class="obs-title">Сигнал ошибок</div>
|
|
2371
|
-
<div class="obs-value" style="color:${errorSignal ? 'var(--red)' : 'var(--green)'}">${errorSignal}</div>
|
|
2372
|
-
<div class="obs-sub">Сумма упавших тестов из последнего прогона как индикатор нестабильности сигналов.</div>
|
|
2373
|
-
</div>
|
|
2374
|
-
|
|
2375
|
-
<div class="obs-card" style="grid-column:1 / -1">
|
|
2376
|
-
<div class="obs-title">Недостающие структурированные поля (кандидаты)</div>
|
|
2377
|
-
<div class="obs-list">
|
|
2378
|
-
${missingStructured.map(m => `<div class="obs-list-item"><span>${m.relativePath}</span><strong>нужна схема</strong></div>`).join('') || '<div class="obs-sub">Явных кандидатов не найдено</div>'}
|
|
2379
|
-
</div>
|
|
2380
|
-
</div>
|
|
2381
|
-
</div>`;
|
|
2382
|
-
}
|
|
2482
|
+
<div class="obs-card" style="margin-bottom:12px">
|
|
2483
|
+
<div class="obs-title">Каталог источников логов (топ 15)</div>
|
|
2484
|
+
<div class="obs-catalog" style="margin-top:8px;border:none;padding:0">
|
|
2485
|
+
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>пробелы</span><span>не хватает</span><span>действие</span></div>
|
|
2486
|
+
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2487
|
+
</div>
|
|
2488
|
+
</div>`;
|
|
2489
|
+
}
|
|
2383
2490
|
|
|
2384
2491
|
function backToFeatureDetail() {
|
|
2385
2492
|
drillTestType = null;
|
|
@@ -2397,15 +2504,15 @@ function toPct(v) {
|
|
|
2397
2504
|
return Math.round((v || 0) * 100) + '%';
|
|
2398
2505
|
}
|
|
2399
2506
|
|
|
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
|
-
];
|
|
2507
|
+
function renderObservabilityOverview(c) {
|
|
2508
|
+
const o = D.observability;
|
|
2509
|
+
if (!o) return '';
|
|
2510
|
+
const metrics = [
|
|
2511
|
+
['Коэффициент шума', toPct(o.metrics.noise_ratio)],
|
|
2512
|
+
['Действия по ошибкам', toPct(o.metrics.error_actionability)],
|
|
2513
|
+
['Полнота структуры', toPct(o.metrics.structured_completeness)],
|
|
2514
|
+
['Покрытие ключевых сценариев', toPct(o.metrics.coverage_of_key_flows)],
|
|
2515
|
+
];
|
|
2409
2516
|
const noisy = (o.topNoisyPatterns || []).slice(0, 5).map(i =>
|
|
2410
2517
|
`<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
2518
|
).join('') || '<div class="obs-row">Нет шумных паттернов</div>';
|
|
@@ -2418,27 +2525,27 @@ function renderObservabilityOverview(c) {
|
|
|
2418
2525
|
`<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
2526
|
).join('');
|
|
2420
2527
|
|
|
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>
|
|
2528
|
+
return `
|
|
2529
|
+
<div class="observability-panel">
|
|
2530
|
+
<div class="observability-title">Наблюдаемость</div>
|
|
2531
|
+
<div class="obs-metrics">
|
|
2532
|
+
${metrics.map(([l,v]) => `<div class="obs-metric"><div class="obs-metric-v">${v}</div><div class="obs-metric-l">${l}</div></div>`).join('')}
|
|
2533
|
+
</div>
|
|
2534
|
+
<div class="obs-columns">
|
|
2535
|
+
<div class="obs-list">
|
|
2536
|
+
<h4>Топ шумных паттернов</h4>
|
|
2537
|
+
${noisy}
|
|
2538
|
+
</div>
|
|
2539
|
+
<div class="obs-list">
|
|
2540
|
+
<h4>Отсутствующие критичные логи</h4>
|
|
2541
|
+
${missing}
|
|
2542
|
+
</div>
|
|
2543
|
+
</div>
|
|
2544
|
+
<div class="obs-catalog">
|
|
2545
|
+
<h4>Каталог источников логов (топ 10)</h4>
|
|
2546
|
+
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>частота</span><span>владелец</span><span>действие</span></div>
|
|
2547
|
+
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2548
|
+
</div>
|
|
2442
2549
|
<div class="obs-row" style="margin-top:8px">
|
|
2443
2550
|
Классификация: мусор ${o.classification.trash} · полезно ${o.classification.useful} · критично ${o.classification.critical}
|
|
2444
2551
|
</div>
|
|
@@ -2476,7 +2583,7 @@ function renderFeatureCards(c) {
|
|
|
2476
2583
|
</div>
|
|
2477
2584
|
</div>` : '';
|
|
2478
2585
|
|
|
2479
|
-
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
2586
|
+
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
2480
2587
|
const grid = document.getElementById('featGrid');
|
|
2481
2588
|
|
|
2482
2589
|
list.forEach(f => {
|
|
@@ -2781,12 +2888,12 @@ function renderFeatureDetail(c) {
|
|
|
2781
2888
|
return;
|
|
2782
2889
|
}
|
|
2783
2890
|
|
|
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;
|
|
2891
|
+
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
2892
|
+
const src = mods.filter(m => m.type !== 'test');
|
|
2893
|
+
const untestedSrc = src.filter(m => !m.hasTests);
|
|
2894
|
+
const tst = mods.filter(m => m.type === 'test');
|
|
2895
|
+
const testedCount = src.filter(m => m.hasTests).length;
|
|
2896
|
+
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
2790
2897
|
|
|
2791
2898
|
const unitCount = feat.unitTestCount ?? tst.filter(m => m.testType === 'unit').length;
|
|
2792
2899
|
const integrationCount = feat.integrationTestCount ?? tst.filter(m => m.testType === 'integration').length;
|
|
@@ -2798,32 +2905,32 @@ function renderFeatureDetail(c) {
|
|
|
2798
2905
|
const integrationFailed = tst.filter(m => m.testType === 'integration' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
2799
2906
|
const e2eFailed = tst.filter(m => m.testType === 'e2e' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
2800
2907
|
|
|
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;
|
|
2908
|
+
// Determine what list to show based on active tab
|
|
2909
|
+
// null or 'source' → source files; test type → test files of that type
|
|
2910
|
+
const activeTab = drillTestType || 'source';
|
|
2911
|
+
const sourceList = showOnlyUntestedInFeature ? untestedSrc : src;
|
|
2912
|
+
const listFiles = activeTab === 'source' ? sourceList : tst.filter(m => m.testType === activeTab);
|
|
2913
|
+
const isTestList = activeTab !== 'source';
|
|
2914
|
+
const showUntestedToggle = activeTab === 'source';
|
|
2915
|
+
const meta = TEST_TYPE_META[activeTab];
|
|
2916
|
+
const listLabel = meta
|
|
2917
|
+
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
2918
|
+
: `📁 Файлы фичи (${listFiles.length})`;
|
|
2919
|
+
|
|
2920
|
+
const q = searchQuery.toLowerCase();
|
|
2921
|
+
const filtered = q ? listFiles.filter(m =>
|
|
2922
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2923
|
+
) : listFiles;
|
|
2924
|
+
const rowsRenderKey = `feature:${drillFeatureKey}:${activeTab}:${showOnlyUntestedInFeature ? 1 : 0}:${q}`;
|
|
2925
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2926
|
+
const selectableSource = !isTestList && !!D.agent;
|
|
2927
|
+
const visibleSourcePaths = selectableSource
|
|
2928
|
+
? visibleRows.map(m => m.relativePath.replace(/\\/g, '/'))
|
|
2929
|
+
: [];
|
|
2930
|
+
const selectedVisible = selectableSource
|
|
2931
|
+
? selectedFilesForFeature(drillFeatureKey, visibleRows.map(m => m.relativePath))
|
|
2932
|
+
: [];
|
|
2933
|
+
const selectedCount = selectedVisible.length;
|
|
2827
2934
|
|
|
2828
2935
|
c.innerHTML = `
|
|
2829
2936
|
<div class="drill-header">
|
|
@@ -2849,51 +2956,51 @@ function renderFeatureDetail(c) {
|
|
|
2849
2956
|
${testTypeCard('integration', 'Integration', '🔗', '#58a6ff', integrationCount, activeTab === 'integration', drillFeatureKey, integrationFailed)}
|
|
2850
2957
|
${testTypeCard('e2e', 'E2E', '🎭', '#d2a8ff', e2eCount, activeTab === 'e2e', drillFeatureKey, e2eFailed)}
|
|
2851
2958
|
</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>
|
|
2959
|
+
|
|
2960
|
+
<div class="drill-section-label">${listLabel}</div>
|
|
2961
|
+
${showUntestedToggle ? `
|
|
2962
|
+
<div class="feature-file-filters">
|
|
2963
|
+
<label class="feature-filter-toggle">
|
|
2964
|
+
<input type="checkbox" id="untestedOnlyToggle" ${showOnlyUntestedInFeature ? 'checked' : ''}>
|
|
2965
|
+
<span>Только без тестов</span>
|
|
2966
|
+
<span class="feature-filter-meta">(${untestedSrc.length})</span>
|
|
2967
|
+
</label>
|
|
2968
|
+
</div>` : ''}
|
|
2969
|
+
${selectableSource ? `
|
|
2970
|
+
<div class="bulk-actions">
|
|
2971
|
+
<span class="bulk-actions-count">Выбрано файлов: <b>${selectedCount}</b></span>
|
|
2972
|
+
<button class="bulk-actions-btn" id="bulkSelectAllBtn" ${visibleSourcePaths.length === 0 ? 'disabled' : ''}>☑ Выбрать все (${visibleSourcePaths.length})</button>
|
|
2866
2973
|
<button class="bulk-actions-btn" id="bulkClearBtn" ${selectedCount === 0 ? 'disabled' : ''}>Снять выбор</button>
|
|
2867
2974
|
<button class="bulk-actions-btn primary" id="bulkWriteTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>✍ Написать тесты для выбранных</button>
|
|
2868
2975
|
<button class="bulk-actions-btn" id="bulkRefreshTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>↻ Актуализировать тесты</button>
|
|
2869
2976
|
</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) {
|
|
2977
|
+
<div class="file-rows" id="fileRows">
|
|
2978
|
+
${filtered.length === 0
|
|
2979
|
+
? `<div style="padding:20px;text-align:center;border:1px dashed var(--border);border-radius:8px;color:var(--dim);font-size:13px">
|
|
2980
|
+
${isTestList
|
|
2981
|
+
? 'Нет тестов этого типа для данной фичи'
|
|
2982
|
+
: (showOnlyUntestedInFeature
|
|
2983
|
+
? 'Все файлы этой фичи уже покрыты тестами'
|
|
2984
|
+
: 'Нет файлов — возможно паттерны в конфиге не совпадают')}
|
|
2985
|
+
</div>`
|
|
2986
|
+
: visibleRows.map(m => fileRow(m, isTestList, drillFeatureKey, selectableSource)).join('')
|
|
2987
|
+
}
|
|
2988
|
+
</div>
|
|
2989
|
+
${hasMoreRows ? `
|
|
2990
|
+
<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)">
|
|
2991
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
2992
|
+
<button class="bulk-actions-btn" id="fileRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
2993
|
+
</div>` : ''}`;
|
|
2994
|
+
|
|
2995
|
+
const untestedOnlyToggle = document.getElementById('untestedOnlyToggle');
|
|
2996
|
+
if (untestedOnlyToggle) {
|
|
2997
|
+
untestedOnlyToggle.onchange = (e) => {
|
|
2998
|
+
showOnlyUntestedInFeature = !!e.target.checked;
|
|
2999
|
+
renderContent();
|
|
3000
|
+
};
|
|
3001
|
+
}
|
|
3002
|
+
|
|
3003
|
+
if (selectableSource) {
|
|
2897
3004
|
const allVisibleSelected = visibleSourcePaths.length > 0 && visibleSourcePaths.every((p) => selectedSourceFiles.has(p));
|
|
2898
3005
|
const bulkSelectAllBtn = document.getElementById('bulkSelectAllBtn');
|
|
2899
3006
|
if (bulkSelectAllBtn) {
|
|
@@ -2928,17 +3035,17 @@ function renderFeatureDetail(c) {
|
|
|
2928
3035
|
renderContent();
|
|
2929
3036
|
};
|
|
2930
3037
|
}
|
|
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 => {
|
|
3038
|
+
}
|
|
3039
|
+
|
|
3040
|
+
const fileRowsLoadMoreBtn = document.getElementById('fileRowsLoadMoreBtn');
|
|
3041
|
+
if (fileRowsLoadMoreBtn) {
|
|
3042
|
+
fileRowsLoadMoreBtn.onclick = () => {
|
|
3043
|
+
increaseFileRowsLimit();
|
|
3044
|
+
renderContent();
|
|
3045
|
+
};
|
|
3046
|
+
}
|
|
3047
|
+
|
|
3048
|
+
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
2942
3049
|
card.onclick = async () => {
|
|
2943
3050
|
const type = card.dataset.testtype;
|
|
2944
3051
|
drillTestType = (type === 'source') ? null : type; // 'source' tab = null state
|
|
@@ -2947,11 +3054,11 @@ function renderFeatureDetail(c) {
|
|
|
2947
3054
|
if (type === 'e2e') {
|
|
2948
3055
|
await loadE2ePlan(drillFeatureKey);
|
|
2949
3056
|
}
|
|
2950
|
-
renderContent();
|
|
2951
|
-
};
|
|
2952
|
-
});
|
|
2953
|
-
bindFileRowsClick(c);
|
|
2954
|
-
}
|
|
3057
|
+
renderContent();
|
|
3058
|
+
};
|
|
3059
|
+
});
|
|
3060
|
+
bindFileRowsClick(c);
|
|
3061
|
+
}
|
|
2955
3062
|
|
|
2956
3063
|
const TEST_TYPE_META = {
|
|
2957
3064
|
unit: { label: 'Unit', icon: '🧪', color: '#e3b341', desc: 'Изолированные тесты функций и модулей' },
|
|
@@ -2970,12 +3077,12 @@ function renderTestTypeDetail(c) {
|
|
|
2970
3077
|
m.featureKeys && m.featureKeys.includes(drillFeatureKey)
|
|
2971
3078
|
);
|
|
2972
3079
|
|
|
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);
|
|
3080
|
+
const q = searchQuery.toLowerCase();
|
|
3081
|
+
const filtered = q ? tests.filter(m =>
|
|
3082
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3083
|
+
) : tests;
|
|
3084
|
+
const rowsRenderKey = `tests:${drillFeatureKey}:${drillTestType}:${q}`;
|
|
3085
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2979
3086
|
|
|
2980
3087
|
c.innerHTML = `
|
|
2981
3088
|
<div class="drill-header">
|
|
@@ -2990,32 +3097,32 @@ function renderTestTypeDetail(c) {
|
|
|
2990
3097
|
</div>
|
|
2991
3098
|
<div style="font-size:12px;color:var(--dim);margin-bottom:16px">${meta.desc}</div>
|
|
2992
3099
|
|
|
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
|
-
}
|
|
3100
|
+
<div class="file-rows" id="fileRows">
|
|
3101
|
+
${filtered.length === 0
|
|
3102
|
+
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
3103
|
+
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
3104
|
+
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
3105
|
+
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
3106
|
+
</div>`
|
|
3107
|
+
: visibleRows.map(m => fileRow(m, true)).join('')
|
|
3108
|
+
}
|
|
3109
|
+
</div>
|
|
3110
|
+
${hasMoreRows ? `
|
|
3111
|
+
<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)">
|
|
3112
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} тест-файлов</span>
|
|
3113
|
+
<button class="bulk-actions-btn" id="testRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3114
|
+
</div>` : ''}`;
|
|
3115
|
+
|
|
3116
|
+
const testRowsLoadMoreBtn = document.getElementById('testRowsLoadMoreBtn');
|
|
3117
|
+
if (testRowsLoadMoreBtn) {
|
|
3118
|
+
testRowsLoadMoreBtn.onclick = () => {
|
|
3119
|
+
increaseFileRowsLimit();
|
|
3120
|
+
renderContent();
|
|
3121
|
+
};
|
|
3122
|
+
}
|
|
3123
|
+
|
|
3124
|
+
bindFileRowsClick(c);
|
|
3125
|
+
}
|
|
3019
3126
|
|
|
3020
3127
|
function renderUnmappedDetail(c) {
|
|
3021
3128
|
const infraSrc = D.modules.filter(m => m.type !== 'test' && m.isInfra);
|
|
@@ -3023,12 +3130,12 @@ function renderUnmappedDetail(c) {
|
|
|
3023
3130
|
m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)
|
|
3024
3131
|
);
|
|
3025
3132
|
|
|
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);
|
|
3133
|
+
const q = searchQuery.toLowerCase();
|
|
3134
|
+
const filtered = q ? unmappedSrc.filter(m =>
|
|
3135
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3136
|
+
) : unmappedSrc;
|
|
3137
|
+
const rowsRenderKey = `unmapped:${q}`;
|
|
3138
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3032
3139
|
|
|
3033
3140
|
// Build prompt text
|
|
3034
3141
|
const featureList = (D.features || []).map(f => ` • ${f.key} — ${f.label}`).join('\n');
|
|
@@ -3069,44 +3176,44 @@ function renderUnmappedDetail(c) {
|
|
|
3069
3176
|
border-radius:6px; color:var(--blue); font-size:12px; cursor:pointer;
|
|
3070
3177
|
">📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)</button>
|
|
3071
3178
|
</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>` : ''}`;
|
|
3179
|
+
<div class="file-rows" id="fileRows">
|
|
3180
|
+
${filtered.length === 0
|
|
3181
|
+
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
3182
|
+
: visibleRows.map(m => fileRow(m)).join('')
|
|
3183
|
+
}
|
|
3184
|
+
</div>
|
|
3185
|
+
${hasMoreRows ? `
|
|
3186
|
+
<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)">
|
|
3187
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
3188
|
+
<button class="bulk-actions-btn" id="unmappedRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3189
|
+
</div>` : ''}`;
|
|
3083
3190
|
|
|
3084
3191
|
const runAgentUnmappedBtn = document.getElementById('runAgentUnmapped');
|
|
3085
3192
|
if (runAgentUnmappedBtn) {
|
|
3086
3193
|
runAgentUnmappedBtn.onclick = () => runAgentTask('map-unmapped');
|
|
3087
3194
|
}
|
|
3088
3195
|
|
|
3089
|
-
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
3196
|
+
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
3090
3197
|
navigator.clipboard.writeText(promptText).then(() => {
|
|
3091
3198
|
this.textContent = '✅ Скопировано!';
|
|
3092
3199
|
this.style.color = 'var(--green)';
|
|
3093
3200
|
setTimeout(() => {
|
|
3094
3201
|
this.textContent = `📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)`;
|
|
3095
3202
|
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
|
-
}
|
|
3203
|
+
}, 3000);
|
|
3204
|
+
});
|
|
3205
|
+
};
|
|
3206
|
+
|
|
3207
|
+
const unmappedRowsLoadMoreBtn = document.getElementById('unmappedRowsLoadMoreBtn');
|
|
3208
|
+
if (unmappedRowsLoadMoreBtn) {
|
|
3209
|
+
unmappedRowsLoadMoreBtn.onclick = () => {
|
|
3210
|
+
increaseFileRowsLimit();
|
|
3211
|
+
renderContent();
|
|
3212
|
+
};
|
|
3213
|
+
}
|
|
3214
|
+
|
|
3215
|
+
bindFileRowsClick(c);
|
|
3216
|
+
}
|
|
3110
3217
|
|
|
3111
3218
|
function fileRow(m, isTest = false, featureKey = null, selectable = false) {
|
|
3112
3219
|
const relPath = m.relativePath.replace(/\\/g, '/');
|
|
@@ -3196,33 +3303,33 @@ function toggleSourceSelection(relPath, checked) {
|
|
|
3196
3303
|
renderContent();
|
|
3197
3304
|
}
|
|
3198
3305
|
|
|
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');
|
|
3306
|
+
function renderModuleGrid(c) {
|
|
3307
|
+
const q = searchQuery.toLowerCase();
|
|
3308
|
+
const list = D.modules.filter(m => {
|
|
3309
|
+
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
3310
|
+
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
3311
|
+
return true;
|
|
3312
|
+
});
|
|
3313
|
+
const typeKey = [...activeTypes].sort().join(',');
|
|
3314
|
+
const rowsRenderKey = `modules:${contextMode}:${view}:${typeKey}:${q}`;
|
|
3315
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(list, rowsRenderKey);
|
|
3316
|
+
|
|
3317
|
+
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
3318
|
+
|
|
3319
|
+
c.innerHTML = `
|
|
3320
|
+
<div class="module-grid" id="modGrid"></div>
|
|
3321
|
+
${hasMoreRows ? `
|
|
3322
|
+
<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)">
|
|
3323
|
+
<span>Показано ${visibleRows.length} из ${list.length} модулей</span>
|
|
3324
|
+
<button class="bulk-actions-btn" id="moduleRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3325
|
+
</div>` : ''}
|
|
3326
|
+
`;
|
|
3327
|
+
const grid = document.getElementById('modGrid');
|
|
3328
|
+
|
|
3329
|
+
visibleRows.forEach(m => {
|
|
3330
|
+
const cov = m.coverage?.lines;
|
|
3331
|
+
const isActive = activePanelKey === m.id;
|
|
3332
|
+
const card = document.createElement('div');
|
|
3226
3333
|
card.className = 'module-card' + (isActive ? ' active' : '');
|
|
3227
3334
|
card.innerHTML = `
|
|
3228
3335
|
<div class="module-name">${m.name}</div>
|
|
@@ -3234,18 +3341,18 @@ function renderModuleGrid(c) {
|
|
|
3234
3341
|
<span class="badge ${m.hasTests ? 'badge-green' : 'badge-red'}">${m.hasTests ? '✓' : '✗'}</span>
|
|
3235
3342
|
</div>
|
|
3236
3343
|
${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
|
-
}
|
|
3344
|
+
card.onclick = () => openModulePanel(m);
|
|
3345
|
+
grid.appendChild(card);
|
|
3346
|
+
});
|
|
3347
|
+
|
|
3348
|
+
const moduleRowsLoadMoreBtn = document.getElementById('moduleRowsLoadMoreBtn');
|
|
3349
|
+
if (moduleRowsLoadMoreBtn) {
|
|
3350
|
+
moduleRowsLoadMoreBtn.onclick = () => {
|
|
3351
|
+
increaseFileRowsLimit();
|
|
3352
|
+
renderContent();
|
|
3353
|
+
};
|
|
3354
|
+
}
|
|
3355
|
+
}
|
|
3249
3356
|
|
|
3250
3357
|
// ─── Panels ───────────────────────────────────────────────────────────────────
|
|
3251
3358
|
function openFeaturePanel(key) {
|
|
@@ -3456,29 +3563,29 @@ function openUnmappedPanel(files, infraFiles) {
|
|
|
3456
3563
|
document.getElementById('panel').classList.add('open');
|
|
3457
3564
|
}
|
|
3458
3565
|
|
|
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 => {
|
|
3566
|
+
function closePanel() {
|
|
3567
|
+
activePanelKey = null;
|
|
3568
|
+
document.getElementById('panel').classList.remove('open');
|
|
3569
|
+
renderContent();
|
|
3570
|
+
}
|
|
3571
|
+
|
|
3572
|
+
function applyTerminalFiltersFromUi() {
|
|
3573
|
+
terminalSearchQuery = document.getElementById('agentSearchInput')?.value || '';
|
|
3574
|
+
terminalSearchErrorsOnly = !!document.getElementById('agentSearchErrorsOnly')?.checked;
|
|
3575
|
+
terminalSearchCurrentRunOnly = !!document.getElementById('agentSearchCurrentRunOnly')?.checked;
|
|
3576
|
+
terminalSearchRegex = !!document.getElementById('agentSearchRegex')?.checked;
|
|
3577
|
+
renderActiveSession();
|
|
3578
|
+
}
|
|
3579
|
+
|
|
3580
|
+
['agentSearchInput', 'agentSearchErrorsOnly', 'agentSearchCurrentRunOnly', 'agentSearchRegex'].forEach((id) => {
|
|
3581
|
+
const el = document.getElementById(id);
|
|
3582
|
+
if (!el) return;
|
|
3583
|
+
const eventName = id === 'agentSearchInput' ? 'input' : 'change';
|
|
3584
|
+
el.addEventListener(eventName, applyTerminalFiltersFromUi);
|
|
3585
|
+
});
|
|
3586
|
+
|
|
3587
|
+
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
3588
|
+
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
3482
3589
|
tab.onclick = () => {
|
|
3483
3590
|
if (contextMode !== 'qa') return;
|
|
3484
3591
|
if (tab.classList.contains('disabled')) return;
|
|
@@ -3528,199 +3635,199 @@ window.addEventListener('popstate', () => {
|
|
|
3528
3635
|
});
|
|
3529
3636
|
|
|
3530
3637
|
// ─── 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();
|
|
3638
|
+
function setLiveDot(color, title) {
|
|
3639
|
+
const dot = document.getElementById('liveDot');
|
|
3640
|
+
dot.style.background = color;
|
|
3641
|
+
dot.title = title;
|
|
3642
|
+
}
|
|
3643
|
+
|
|
3644
|
+
function scheduleRefreshData(delayMs = 120) {
|
|
3645
|
+
if (refreshDataTimer) return;
|
|
3646
|
+
refreshDataTimer = setTimeout(() => {
|
|
3647
|
+
refreshDataTimer = null;
|
|
3648
|
+
void refreshData();
|
|
3649
|
+
}, delayMs);
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
async function refreshData() {
|
|
3653
|
+
if (refreshDataInFlight) {
|
|
3654
|
+
refreshDataQueued = true;
|
|
3655
|
+
return;
|
|
3656
|
+
}
|
|
3657
|
+
refreshDataInFlight = true;
|
|
3658
|
+
try {
|
|
3659
|
+
const res = await fetch('/api/data');
|
|
3660
|
+
D = await res.json();
|
|
3554
3661
|
|
|
3555
3662
|
// Update header timestamp
|
|
3556
3663
|
document.getElementById('scannedAt').textContent =
|
|
3557
3664
|
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');
|
|
3665
|
+
|
|
3666
|
+
renderStats();
|
|
3667
|
+
renderSidebar();
|
|
3668
|
+
updateAgentBtn();
|
|
3669
|
+
updateAgentRightsInfo();
|
|
3670
|
+
|
|
3671
|
+
// Re-render drill-down or re-open panel
|
|
3672
|
+
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
3566
3673
|
if (panelOpen && activePanelKey) {
|
|
3567
3674
|
if (drillFeatureKey === '__unmapped__') {
|
|
3568
3675
|
renderContent(); // already routes to renderUnmappedDetail
|
|
3569
3676
|
} else if (view === 'features' && D.features) {
|
|
3570
3677
|
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
|
-
}
|
|
3678
|
+
} else {
|
|
3679
|
+
const m = D.modules.find(m => m.id === activePanelKey);
|
|
3680
|
+
if (m) openModulePanel(m);
|
|
3681
|
+
else closePanel();
|
|
3682
|
+
}
|
|
3683
|
+
} else {
|
|
3684
|
+
renderContent();
|
|
3685
|
+
}
|
|
3686
|
+
|
|
3687
|
+
// Brief green flash on the dot to signal fresh data
|
|
3688
|
+
setLiveDot('#3fb950', 'Live — обновлено только что');
|
|
3689
|
+
setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
|
|
3690
|
+
void loadAgentState();
|
|
3691
|
+
} catch (err) {
|
|
3692
|
+
console.error('Refresh failed:', err);
|
|
3693
|
+
} finally {
|
|
3694
|
+
refreshDataInFlight = false;
|
|
3695
|
+
if (refreshDataQueued) {
|
|
3696
|
+
refreshDataQueued = false;
|
|
3697
|
+
scheduleRefreshData(120);
|
|
3698
|
+
}
|
|
3699
|
+
}
|
|
3700
|
+
}
|
|
3701
|
+
|
|
3702
|
+
function connectSSE() {
|
|
3703
|
+
const es = new EventSource('/api/events');
|
|
3704
|
+
|
|
3705
|
+
es.onopen = () => {
|
|
3706
|
+
setLiveDot('#3fb950', 'Live — автообновление включено');
|
|
3707
|
+
void loadAgentState();
|
|
3708
|
+
};
|
|
3709
|
+
|
|
3710
|
+
es.addEventListener('data-updated', () => {
|
|
3711
|
+
setLiveDot('#e3b341', 'Обновляю данные…');
|
|
3712
|
+
scheduleRefreshData();
|
|
3713
|
+
});
|
|
3714
|
+
|
|
3715
|
+
es.addEventListener('agent-queue-updated', (e) => {
|
|
3716
|
+
const payload = JSON.parse(e.data || '{}');
|
|
3717
|
+
agentQueueState = Array.isArray(payload.queue) ? payload.queue : [];
|
|
3718
|
+
refreshPathActivityFromState();
|
|
3719
|
+
updateQueueBadge(agentQueueState.length);
|
|
3720
|
+
renderContent();
|
|
3721
|
+
});
|
|
3722
|
+
|
|
3723
|
+
es.addEventListener('agent-run-created', (e) => {
|
|
3724
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3725
|
+
upsertRunState(run);
|
|
3726
|
+
});
|
|
3727
|
+
|
|
3728
|
+
es.addEventListener('agent-run-updated', (e) => {
|
|
3729
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3730
|
+
if (!run) return;
|
|
3731
|
+
if (run.runId && agentQueueState.length > 0) {
|
|
3732
|
+
agentQueueState = agentQueueState.filter((q) => q.runId !== run.runId || run.phase === 'queued');
|
|
3733
|
+
updateQueueBadge(agentQueueState.length);
|
|
3734
|
+
}
|
|
3735
|
+
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
3736
|
+
agentActiveRun = run;
|
|
3737
|
+
const startedPaths = getTaskFilePaths(run.task, run.featureKey, run.filePath, run.selectedFilePaths);
|
|
3738
|
+
startedPaths.forEach((p) => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3739
|
+
renderContent();
|
|
3740
|
+
}
|
|
3741
|
+
updateSessionFromRun(run);
|
|
3742
|
+
});
|
|
3743
|
+
|
|
3744
|
+
es.addEventListener('agent-run-finished', (e) => {
|
|
3745
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3746
|
+
if (!run) return;
|
|
3747
|
+
updateSessionFromRun(run);
|
|
3748
|
+
refreshPathActivityFromState();
|
|
3749
|
+
renderContent();
|
|
3696
3750
|
});
|
|
3697
3751
|
|
|
3698
|
-
es.addEventListener('agent-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
|
|
3706
|
-
|
|
3752
|
+
es.addEventListener('agent-queued', (e) => {
|
|
3753
|
+
const { runId, queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3754
|
+
updateQueueBadge(queueLength);
|
|
3755
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
3756
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
3757
|
+
// Track queued paths for spinner
|
|
3758
|
+
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
3759
|
+
renderContent();
|
|
3760
|
+
// Append queue notification to the currently running session (or active)
|
|
3761
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3762
|
+
if (targetId) {
|
|
3763
|
+
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
3764
|
+
}
|
|
3765
|
+
void loadAgentState();
|
|
3766
|
+
});
|
|
3767
|
+
|
|
3768
|
+
es.addEventListener('agent-started', (e) => {
|
|
3769
|
+
setAgentRunning(true);
|
|
3770
|
+
const { runId, title, queueLength = 0, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3771
|
+
updateQueueBadge(queueLength);
|
|
3772
|
+
currentRunId = runId || null;
|
|
3773
|
+
// Move paths from queued → running (current task)
|
|
3774
|
+
const startedPaths = getTaskFilePaths(task, featureKey, filePath, selectedFilePaths);
|
|
3775
|
+
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3776
|
+
renderContent();
|
|
3777
|
+
// Close previous session (queue case: agent-done not fired between tasks)
|
|
3778
|
+
if (runningSessionId) {
|
|
3779
|
+
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
3780
|
+
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
3781
|
+
}
|
|
3782
|
+
const id = runId ? ensureSessionForRun({ runId, title, phase: 'running' }, true) : createSession(title);
|
|
3783
|
+
runningSessionId = id;
|
|
3784
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
3785
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
3786
|
+
});
|
|
3787
|
+
|
|
3788
|
+
es.addEventListener('agent-output', (e) => {
|
|
3789
|
+
const { runId, line, isError, isDim } = JSON.parse(e.data);
|
|
3790
|
+
let targetId = runningSessionId;
|
|
3791
|
+
if (runId) {
|
|
3792
|
+
targetId = runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'running' });
|
|
3793
|
+
if (targetId) runningSessionId = targetId;
|
|
3794
|
+
}
|
|
3795
|
+
if (targetId) {
|
|
3796
|
+
appendToSession(targetId, line, !!isError, !!isDim);
|
|
3797
|
+
if (activeSessionId === targetId) {
|
|
3798
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
3799
|
+
}
|
|
3800
|
+
} else {
|
|
3801
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
3802
|
+
}
|
|
3803
|
+
});
|
|
3804
|
+
|
|
3805
|
+
es.addEventListener('agent-done', () => {
|
|
3806
|
+
setAgentRunning(false);
|
|
3807
|
+
if (agentQueueState.length === 0) updateQueueBadge(0);
|
|
3808
|
+
agentRunningPaths.clear();
|
|
3809
|
+
if (runningSessionId) {
|
|
3810
|
+
updateSessionStatus(runningSessionId, 'ok');
|
|
3811
|
+
if (activeSessionId === runningSessionId) {
|
|
3812
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
3813
|
+
}
|
|
3707
3814
|
runningSessionId = null;
|
|
3708
3815
|
}
|
|
3709
3816
|
renderContent();
|
|
3710
3817
|
});
|
|
3711
3818
|
|
|
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;
|
|
3819
|
+
es.addEventListener('agent-summary', (e) => {
|
|
3820
|
+
const {
|
|
3821
|
+
runId,
|
|
3822
|
+
validationStats = null,
|
|
3823
|
+
passed = 0, failed = 0, files = [],
|
|
3824
|
+
testedFileCount = files.length,
|
|
3825
|
+
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
3826
|
+
failedFileCount = failed > 0 ? 1 : 0,
|
|
3827
|
+
autoFixQueued = false,
|
|
3828
|
+
} = JSON.parse(e.data);
|
|
3829
|
+
const coverageOk = !validationStats || ((validationStats.notCovered || 0) === 0 && (validationStats.blocked || 0) === 0);
|
|
3830
|
+
const allOk = failed === 0 && coverageOk;
|
|
3724
3831
|
const box = document.createElement('div');
|
|
3725
3832
|
box.style.cssText = `
|
|
3726
3833
|
margin: 10px 0 4px;
|
|
@@ -3737,33 +3844,33 @@ function connectSSE() {
|
|
|
3737
3844
|
<div style="font-size:11px;color:var(--muted);margin-top:4px">
|
|
3738
3845
|
Файлы: ${testedFileCount} • passed: ${passedFileCount} • failed: ${failedFileCount}
|
|
3739
3846
|
</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');
|
|
3847
|
+
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3848
|
+
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3849
|
+
</div>
|
|
3850
|
+
${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>` : ''}
|
|
3851
|
+
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
3852
|
+
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
3853
|
+
`;
|
|
3854
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3855
|
+
if (targetId) {
|
|
3856
|
+
appendToSession(targetId, box);
|
|
3857
|
+
// Mark session status from test results immediately
|
|
3858
|
+
if (runningSessionId === targetId) {
|
|
3859
|
+
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
3753
3860
|
}
|
|
3754
3861
|
}
|
|
3755
3862
|
});
|
|
3756
3863
|
|
|
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) {
|
|
3864
|
+
es.addEventListener('agent-error', (e) => {
|
|
3865
|
+
const { runId, message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
3866
|
+
if (!runId) setAgentRunning(false);
|
|
3867
|
+
agentRunningPaths.clear();
|
|
3868
|
+
agentQueuedPaths.clear();
|
|
3869
|
+
renderContent();
|
|
3870
|
+
|
|
3871
|
+
// Build error node (plain text or auth/install box)
|
|
3872
|
+
let node = null;
|
|
3873
|
+
if (authRequired || notInstalled) {
|
|
3767
3874
|
node = document.createElement('div');
|
|
3768
3875
|
node.style.cssText = 'margin:10px 0 4px;padding:10px 14px;border-radius:8px;border:1px solid var(--red);background:#2a0d0d;font-family:inherit;';
|
|
3769
3876
|
node.innerHTML = `
|
|
@@ -3771,14 +3878,14 @@ function connectSSE() {
|
|
|
3771
3878
|
${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
3879
|
${notInstalled ? `<div style="margin-top:6px;font-size:11px;color:var(--dim)">После установки перезапусти viberadar</div>` : ''}
|
|
3773
3880
|
`;
|
|
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
|
-
}
|
|
3881
|
+
}
|
|
3882
|
+
|
|
3883
|
+
// If no session exists yet (startup check fires before any run), create one
|
|
3884
|
+
const targetId = (runId ? (runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'failed' })) : runningSessionId) || (() => {
|
|
3885
|
+
if (authRequired || notInstalled) {
|
|
3886
|
+
const id = createSession('⚠️ Проверка агента', 'error');
|
|
3887
|
+
return id;
|
|
3888
|
+
}
|
|
3782
3889
|
return activeSessionId;
|
|
3783
3890
|
})();
|
|
3784
3891
|
|
|
@@ -3789,12 +3896,12 @@ function connectSSE() {
|
|
|
3789
3896
|
appendToSession(targetId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
3790
3897
|
}
|
|
3791
3898
|
updateSessionStatus(targetId, 'error');
|
|
3792
|
-
if (activeSessionId === targetId) {
|
|
3793
|
-
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3794
|
-
}
|
|
3795
|
-
}
|
|
3796
|
-
if (!runId && runningSessionId) runningSessionId = null;
|
|
3797
|
-
});
|
|
3899
|
+
if (activeSessionId === targetId) {
|
|
3900
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
if (!runId && runningSessionId) runningSessionId = null;
|
|
3904
|
+
});
|
|
3798
3905
|
|
|
3799
3906
|
es.addEventListener('tests-started', (e) => {
|
|
3800
3907
|
const { testType, count } = JSON.parse(e.data);
|
|
@@ -3862,24 +3969,24 @@ function connectSSE() {
|
|
|
3862
3969
|
}
|
|
3863
3970
|
});
|
|
3864
3971
|
|
|
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'] });
|
|
3972
|
+
es.onerror = () => {
|
|
3973
|
+
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
3974
|
+
es.close();
|
|
3975
|
+
setTimeout(connectSSE, 3000);
|
|
3976
|
+
};
|
|
3977
|
+
}
|
|
3978
|
+
|
|
3979
|
+
init().then(async () => {
|
|
3980
|
+
restoreSessions();
|
|
3981
|
+
await loadAgentState();
|
|
3982
|
+
connectSSE();
|
|
3983
|
+
applyTerminalFiltersFromUi();
|
|
3984
|
+
updateToolbarButtonsState();
|
|
3985
|
+
// Sync content padding with terminal panel open state automatically
|
|
3986
|
+
new MutationObserver(() => {
|
|
3987
|
+
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
3988
|
+
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
3989
|
+
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
3883
3990
|
});
|
|
3884
3991
|
</script>
|
|
3885
3992
|
</body>
|