viberadar 0.3.62 → 0.3.63
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +31 -0
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +1344 -1341
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -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,117 +769,117 @@
|
|
|
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); }
|
|
858
|
-
.agent-summary-matrix {
|
|
859
|
-
display: none;
|
|
860
|
-
border-bottom: 1px solid var(--border);
|
|
861
|
-
background: #090f17;
|
|
862
|
-
padding: 8px 12px;
|
|
863
|
-
max-height: 150px;
|
|
864
|
-
overflow: auto;
|
|
865
|
-
font-size: 11px;
|
|
866
|
-
}
|
|
867
|
-
.agent-summary-title {
|
|
868
|
-
color: var(--muted);
|
|
869
|
-
font-weight: 600;
|
|
870
|
-
margin-bottom: 6px;
|
|
871
|
-
}
|
|
872
|
-
.agent-summary-row {
|
|
873
|
-
display: grid;
|
|
874
|
-
grid-template-columns: 60px 1fr auto;
|
|
875
|
-
gap: 8px;
|
|
876
|
-
margin-bottom: 4px;
|
|
877
|
-
align-items: center;
|
|
878
|
-
}
|
|
879
|
-
.agent-summary-status-covered { color: var(--green); }
|
|
880
|
-
.agent-summary-status-not-covered { color: var(--red); }
|
|
881
|
-
.agent-summary-status-blocked { color: var(--yellow); }
|
|
882
|
-
.agent-summary-status-infra { color: var(--dim); }
|
|
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
|
+
.agent-summary-matrix {
|
|
859
|
+
display: none;
|
|
860
|
+
border-bottom: 1px solid var(--border);
|
|
861
|
+
background: #090f17;
|
|
862
|
+
padding: 8px 12px;
|
|
863
|
+
max-height: 150px;
|
|
864
|
+
overflow: auto;
|
|
865
|
+
font-size: 11px;
|
|
866
|
+
}
|
|
867
|
+
.agent-summary-title {
|
|
868
|
+
color: var(--muted);
|
|
869
|
+
font-weight: 600;
|
|
870
|
+
margin-bottom: 6px;
|
|
871
|
+
}
|
|
872
|
+
.agent-summary-row {
|
|
873
|
+
display: grid;
|
|
874
|
+
grid-template-columns: 60px 1fr auto;
|
|
875
|
+
gap: 8px;
|
|
876
|
+
margin-bottom: 4px;
|
|
877
|
+
align-items: center;
|
|
878
|
+
}
|
|
879
|
+
.agent-summary-status-covered { color: var(--green); }
|
|
880
|
+
.agent-summary-status-not-covered { color: var(--red); }
|
|
881
|
+
.agent-summary-status-blocked { color: var(--yellow); }
|
|
882
|
+
.agent-summary-status-infra { color: var(--dim); }
|
|
883
883
|
/* ── Console Tabs ───────────────────────────────────────────────────────── */
|
|
884
884
|
.agent-tabs-bar {
|
|
885
885
|
display: flex; align-items: stretch; overflow-x: auto;
|
|
@@ -959,50 +959,50 @@
|
|
|
959
959
|
border-color: var(--accent);
|
|
960
960
|
color: var(--accent);
|
|
961
961
|
}
|
|
962
|
-
.bulk-actions-btn:hover:not(:disabled) { background: var(--bg-hover); }
|
|
963
|
-
.bulk-actions-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
964
|
-
.feature-file-filters {
|
|
965
|
-
display: flex;
|
|
966
|
-
align-items: center;
|
|
967
|
-
gap: 8px;
|
|
968
|
-
margin: 0 0 10px;
|
|
969
|
-
}
|
|
970
|
-
.feature-filter-toggle {
|
|
971
|
-
display: inline-flex;
|
|
972
|
-
align-items: center;
|
|
973
|
-
gap: 8px;
|
|
974
|
-
padding: 6px 10px;
|
|
975
|
-
border-radius: 6px;
|
|
976
|
-
border: 1px solid var(--border);
|
|
977
|
-
background: var(--bg-card);
|
|
978
|
-
color: var(--muted);
|
|
979
|
-
font-size: 12px;
|
|
980
|
-
cursor: pointer;
|
|
981
|
-
user-select: none;
|
|
982
|
-
}
|
|
983
|
-
.feature-filter-toggle:hover { background: var(--bg-hover); color: var(--text); }
|
|
984
|
-
.feature-filter-toggle input {
|
|
985
|
-
width: 14px;
|
|
986
|
-
height: 14px;
|
|
987
|
-
accent-color: var(--blue);
|
|
988
|
-
cursor: pointer;
|
|
989
|
-
}
|
|
990
|
-
.feature-filter-meta { color: var(--dim); font-size: 11px; }
|
|
991
|
-
.agent-terminal {
|
|
992
|
-
flex: 1;
|
|
993
|
-
overflow-y: auto;
|
|
994
|
-
padding: 10px 16px;
|
|
995
|
-
font-family: 'Consolas', 'Menlo', 'Courier New', monospace;
|
|
962
|
+
.bulk-actions-btn:hover:not(:disabled) { background: var(--bg-hover); }
|
|
963
|
+
.bulk-actions-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
964
|
+
.feature-file-filters {
|
|
965
|
+
display: flex;
|
|
966
|
+
align-items: center;
|
|
967
|
+
gap: 8px;
|
|
968
|
+
margin: 0 0 10px;
|
|
969
|
+
}
|
|
970
|
+
.feature-filter-toggle {
|
|
971
|
+
display: inline-flex;
|
|
972
|
+
align-items: center;
|
|
973
|
+
gap: 8px;
|
|
974
|
+
padding: 6px 10px;
|
|
975
|
+
border-radius: 6px;
|
|
976
|
+
border: 1px solid var(--border);
|
|
977
|
+
background: var(--bg-card);
|
|
978
|
+
color: var(--muted);
|
|
979
|
+
font-size: 12px;
|
|
980
|
+
cursor: pointer;
|
|
981
|
+
user-select: none;
|
|
982
|
+
}
|
|
983
|
+
.feature-filter-toggle:hover { background: var(--bg-hover); color: var(--text); }
|
|
984
|
+
.feature-filter-toggle input {
|
|
985
|
+
width: 14px;
|
|
986
|
+
height: 14px;
|
|
987
|
+
accent-color: var(--blue);
|
|
988
|
+
cursor: pointer;
|
|
989
|
+
}
|
|
990
|
+
.feature-filter-meta { color: var(--dim); font-size: 11px; }
|
|
991
|
+
.agent-terminal {
|
|
992
|
+
flex: 1;
|
|
993
|
+
overflow-y: auto;
|
|
994
|
+
padding: 10px 16px;
|
|
995
|
+
font-family: 'Consolas', 'Menlo', 'Courier New', monospace;
|
|
996
996
|
font-size: 12px;
|
|
997
997
|
line-height: 1.5;
|
|
998
998
|
}
|
|
999
|
-
.agent-line { color: #c9d1d9; }
|
|
1000
|
-
.agent-line.err { color: var(--red); }
|
|
1001
|
-
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
1002
|
-
.agent-line.match { background: rgba(31, 111, 235, 0.22); }
|
|
1003
|
-
.agent-line.command { color: #79c0ff; }
|
|
1004
|
-
|
|
1005
|
-
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
999
|
+
.agent-line { color: #c9d1d9; }
|
|
1000
|
+
.agent-line.err { color: var(--red); }
|
|
1001
|
+
.agent-line.dim { color: var(--dim); font-size: 10px; }
|
|
1002
|
+
.agent-line.match { background: rgba(31, 111, 235, 0.22); }
|
|
1003
|
+
.agent-line.command { color: #79c0ff; }
|
|
1004
|
+
|
|
1005
|
+
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
1006
1006
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
1007
1007
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
1008
1008
|
/* ─── E2E Plan UI ─────────────────────────────────────────────────── */
|
|
@@ -1114,35 +1114,35 @@
|
|
|
1114
1114
|
<div id="panelContent"></div>
|
|
1115
1115
|
</div>
|
|
1116
1116
|
|
|
1117
|
-
<div class="agent-panel" id="agentPanel">
|
|
1118
|
-
<div class="agent-panel-header">
|
|
1119
|
-
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
1120
|
-
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
1117
|
+
<div class="agent-panel" id="agentPanel">
|
|
1118
|
+
<div class="agent-panel-header">
|
|
1119
|
+
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
1120
|
+
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
1121
1121
|
<span class="agent-queue-badge" id="agentQueueBadge" style="display:none">📋 <span id="agentQueueCount">0</span> в очереди</span>
|
|
1122
1122
|
<button class="agent-panel-cancel" id="agentQueueClearBtn" onclick="clearAgentQueue()" title="Очистить очередь" style="display:none">🗑 очередь</button>
|
|
1123
1123
|
<button class="agent-panel-cancel" id="agentCancelBtn" onclick="cancelAgent()" title="Сбросить состояние агента" style="display:none">⏹ сброс</button>
|
|
1124
|
-
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
1125
|
-
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
1126
|
-
</div>
|
|
1127
|
-
<div class="agent-toolbar">
|
|
1128
|
-
<input id="agentSearchInput" type="text" placeholder="Поиск по терминалу..." />
|
|
1129
|
-
<label><input id="agentSearchErrorsOnly" type="checkbox" /> only errors</label>
|
|
1130
|
-
<label><input id="agentSearchCurrentRunOnly" type="checkbox" /> current run</label>
|
|
1131
|
-
<label><input id="agentSearchRegex" type="checkbox" /> regex</label>
|
|
1132
|
-
<button class="agent-toolbar-btn" id="btnMatchPrev" onclick="jumpTerminalMatch(-1)">↑ match</button>
|
|
1133
|
-
<button class="agent-toolbar-btn" id="btnMatchNext" onclick="jumpTerminalMatch(1)">↓ match</button>
|
|
1134
|
-
<button class="agent-toolbar-btn" id="btnCmdPrev" onclick="jumpCommand(-1)">↑ command</button>
|
|
1135
|
-
<button class="agent-toolbar-btn" id="btnCmdNext" onclick="jumpCommand(1)">↓ command</button>
|
|
1136
|
-
<button class="agent-toolbar-btn" id="btnErrPrev" onclick="jumpError(-1)">↑ error</button>
|
|
1137
|
-
<button class="agent-toolbar-btn" id="btnErrNext" onclick="jumpError(1)">↓ error</button>
|
|
1138
|
-
<button class="agent-toolbar-btn" id="btnExportRun" onclick="exportActiveRun()">Export run</button>
|
|
1139
|
-
<span class="agent-toolbar-meta" id="agentSearchMeta">0 matches</span>
|
|
1140
|
-
</div>
|
|
1141
|
-
<div class="agent-queue-panel" id="agentQueuePanel"></div>
|
|
1142
|
-
<div class="agent-summary-matrix" id="agentSummaryMatrix"></div>
|
|
1143
|
-
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
1144
|
-
<div class="agent-terminal" id="agentTerminal"></div>
|
|
1145
|
-
</div>
|
|
1124
|
+
<button class="agent-panel-copy" id="agentCopyBtn" onclick="copyTerminalContent()" title="Скопировать содержимое вкладки в буфер обмена">⎘</button>
|
|
1125
|
+
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
1126
|
+
</div>
|
|
1127
|
+
<div class="agent-toolbar">
|
|
1128
|
+
<input id="agentSearchInput" type="text" placeholder="Поиск по терминалу..." />
|
|
1129
|
+
<label><input id="agentSearchErrorsOnly" type="checkbox" /> only errors</label>
|
|
1130
|
+
<label><input id="agentSearchCurrentRunOnly" type="checkbox" /> current run</label>
|
|
1131
|
+
<label><input id="agentSearchRegex" type="checkbox" /> regex</label>
|
|
1132
|
+
<button class="agent-toolbar-btn" id="btnMatchPrev" onclick="jumpTerminalMatch(-1)">↑ match</button>
|
|
1133
|
+
<button class="agent-toolbar-btn" id="btnMatchNext" onclick="jumpTerminalMatch(1)">↓ match</button>
|
|
1134
|
+
<button class="agent-toolbar-btn" id="btnCmdPrev" onclick="jumpCommand(-1)">↑ command</button>
|
|
1135
|
+
<button class="agent-toolbar-btn" id="btnCmdNext" onclick="jumpCommand(1)">↓ command</button>
|
|
1136
|
+
<button class="agent-toolbar-btn" id="btnErrPrev" onclick="jumpError(-1)">↑ error</button>
|
|
1137
|
+
<button class="agent-toolbar-btn" id="btnErrNext" onclick="jumpError(1)">↓ error</button>
|
|
1138
|
+
<button class="agent-toolbar-btn" id="btnExportRun" onclick="exportActiveRun()">Export run</button>
|
|
1139
|
+
<span class="agent-toolbar-meta" id="agentSearchMeta">0 matches</span>
|
|
1140
|
+
</div>
|
|
1141
|
+
<div class="agent-queue-panel" id="agentQueuePanel"></div>
|
|
1142
|
+
<div class="agent-summary-matrix" id="agentSummaryMatrix"></div>
|
|
1143
|
+
<div class="agent-tabs-bar" id="agentTabsBar"></div>
|
|
1144
|
+
<div class="agent-terminal" id="agentTerminal"></div>
|
|
1145
|
+
</div>
|
|
1146
1146
|
|
|
1147
1147
|
<script>
|
|
1148
1148
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
@@ -1151,21 +1151,21 @@ let contextMode = 'qa';
|
|
|
1151
1151
|
let view = 'features';
|
|
1152
1152
|
let searchQuery = '';
|
|
1153
1153
|
let activeTypes = new Set();
|
|
1154
|
-
let activePanelKey = null;
|
|
1155
|
-
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
1156
|
-
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
1157
|
-
let showOnlyUntestedInFeature = false; // source tab in feature detail
|
|
1158
|
-
const selectedSourceFiles = new Set(); // normalized relative paths for batch actions
|
|
1159
|
-
const FILE_ROWS_INITIAL_LIMIT = 250;
|
|
1160
|
-
const FILE_ROWS_LIMIT_STEP = 250;
|
|
1161
|
-
let fileRowsRenderKey = '';
|
|
1162
|
-
let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1163
|
-
let e2ePlan = null; // current E2E plan object
|
|
1164
|
-
let e2ePlanLoading = false;
|
|
1165
|
-
const modeStore = {
|
|
1166
|
-
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1167
|
-
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1168
|
-
};
|
|
1154
|
+
let activePanelKey = null;
|
|
1155
|
+
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
1156
|
+
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
1157
|
+
let showOnlyUntestedInFeature = false; // source tab in feature detail
|
|
1158
|
+
const selectedSourceFiles = new Set(); // normalized relative paths for batch actions
|
|
1159
|
+
const FILE_ROWS_INITIAL_LIMIT = 250;
|
|
1160
|
+
const FILE_ROWS_LIMIT_STEP = 250;
|
|
1161
|
+
let fileRowsRenderKey = '';
|
|
1162
|
+
let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1163
|
+
let e2ePlan = null; // current E2E plan object
|
|
1164
|
+
let e2ePlanLoading = false;
|
|
1165
|
+
const modeStore = {
|
|
1166
|
+
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1167
|
+
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1168
|
+
};
|
|
1169
1169
|
|
|
1170
1170
|
function getModeFromPath(pathname = window.location.pathname) {
|
|
1171
1171
|
if (pathname.startsWith('/radar/observability')) return 'observability';
|
|
@@ -1186,31 +1186,31 @@ function saveModeState(mode) {
|
|
|
1186
1186
|
modeStore[mode] = {
|
|
1187
1187
|
view,
|
|
1188
1188
|
searchQuery,
|
|
1189
|
-
activeTypes: new Set(activeTypes),
|
|
1190
|
-
drillFeatureKey,
|
|
1191
|
-
drillTestType,
|
|
1192
|
-
activePanelKey,
|
|
1193
|
-
showOnlyUntestedInFeature,
|
|
1194
|
-
};
|
|
1195
|
-
}
|
|
1196
|
-
|
|
1197
|
-
function restoreModeState(mode) {
|
|
1198
|
-
const state = modeStore[mode];
|
|
1189
|
+
activeTypes: new Set(activeTypes),
|
|
1190
|
+
drillFeatureKey,
|
|
1191
|
+
drillTestType,
|
|
1192
|
+
activePanelKey,
|
|
1193
|
+
showOnlyUntestedInFeature,
|
|
1194
|
+
};
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
function restoreModeState(mode) {
|
|
1198
|
+
const state = modeStore[mode];
|
|
1199
1199
|
view = state.view;
|
|
1200
1200
|
searchQuery = state.searchQuery;
|
|
1201
|
-
activeTypes = new Set(state.activeTypes);
|
|
1202
|
-
drillFeatureKey = state.drillFeatureKey;
|
|
1203
|
-
drillTestType = state.drillTestType;
|
|
1204
|
-
activePanelKey = state.activePanelKey;
|
|
1205
|
-
showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
|
|
1206
|
-
}
|
|
1201
|
+
activeTypes = new Set(state.activeTypes);
|
|
1202
|
+
drillFeatureKey = state.drillFeatureKey;
|
|
1203
|
+
drillTestType = state.drillTestType;
|
|
1204
|
+
activePanelKey = state.activePanelKey;
|
|
1205
|
+
showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
|
|
1206
|
+
}
|
|
1207
1207
|
|
|
1208
1208
|
function switchMode(nextMode) {
|
|
1209
1209
|
if (nextMode === contextMode) return;
|
|
1210
1210
|
saveModeState(contextMode);
|
|
1211
1211
|
contextMode = nextMode;
|
|
1212
1212
|
restoreModeState(contextMode);
|
|
1213
|
-
if (contextMode === 'observability') {
|
|
1213
|
+
if (contextMode === 'observability') {
|
|
1214
1214
|
view = 'files';
|
|
1215
1215
|
drillFeatureKey = null;
|
|
1216
1216
|
drillTestType = null;
|
|
@@ -1224,11 +1224,11 @@ function switchMode(nextMode) {
|
|
|
1224
1224
|
renderSidebar();
|
|
1225
1225
|
renderContent();
|
|
1226
1226
|
}
|
|
1227
|
-
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1228
|
-
let runAllRunning = false;
|
|
1229
|
-
let refreshDataInFlight = false;
|
|
1230
|
-
let refreshDataQueued = false;
|
|
1231
|
-
let refreshDataTimer = null;
|
|
1227
|
+
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1228
|
+
let runAllRunning = false;
|
|
1229
|
+
let refreshDataInFlight = false;
|
|
1230
|
+
let refreshDataQueued = false;
|
|
1231
|
+
let refreshDataTimer = null;
|
|
1232
1232
|
|
|
1233
1233
|
function escapeHtml(text) {
|
|
1234
1234
|
return String(text || '')
|
|
@@ -1267,42 +1267,42 @@ function setFeatureDrill(featureKey, syncHash = true) {
|
|
|
1267
1267
|
renderContent();
|
|
1268
1268
|
}
|
|
1269
1269
|
|
|
1270
|
-
function selectedFilesForFeature(featureKey, visibleSourceFiles = null) {
|
|
1271
|
-
const visibleSet = visibleSourceFiles ? new Set(visibleSourceFiles.map(p => p.replace(/\\/g, '/'))) : null;
|
|
1272
|
-
return Array.from(selectedSourceFiles).filter((relPath) => {
|
|
1270
|
+
function selectedFilesForFeature(featureKey, visibleSourceFiles = null) {
|
|
1271
|
+
const visibleSet = visibleSourceFiles ? new Set(visibleSourceFiles.map(p => p.replace(/\\/g, '/'))) : null;
|
|
1272
|
+
return Array.from(selectedSourceFiles).filter((relPath) => {
|
|
1273
1273
|
if (visibleSet && !visibleSet.has(relPath)) return false;
|
|
1274
1274
|
const mod = D?.modules?.find((m) => m.relativePath.replace(/\\/g, '/') === relPath);
|
|
1275
|
-
return !!mod && mod.type !== 'test' && mod.featureKeys?.includes(featureKey);
|
|
1276
|
-
});
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
function getFileRowsWindow(items, renderKey) {
|
|
1280
|
-
if (fileRowsRenderKey !== renderKey) {
|
|
1281
|
-
fileRowsRenderKey = renderKey;
|
|
1282
|
-
fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1283
|
-
}
|
|
1284
|
-
const visibleRows = items.slice(0, fileRowsRenderLimit);
|
|
1285
|
-
const hiddenRows = Math.max(0, items.length - visibleRows.length);
|
|
1286
|
-
return { visibleRows, hiddenRows, hasMoreRows: hiddenRows > 0 };
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
function increaseFileRowsLimit() {
|
|
1290
|
-
fileRowsRenderLimit += FILE_ROWS_LIMIT_STEP;
|
|
1291
|
-
}
|
|
1292
|
-
|
|
1293
|
-
function bindFileRowsClick(container) {
|
|
1294
|
-
const fileRows = container.querySelector('#fileRows');
|
|
1295
|
-
if (!fileRows) return;
|
|
1296
|
-
fileRows.onclick = (event) => {
|
|
1297
|
-
const row = event.target.closest('.file-row[data-id]');
|
|
1298
|
-
if (!row) return;
|
|
1299
|
-
const moduleId = row.dataset.id;
|
|
1300
|
-
const mod = D.modules.find((m) => String(m.id) === moduleId);
|
|
1301
|
-
if (mod) openModulePanel(mod);
|
|
1302
|
-
};
|
|
1303
|
-
}
|
|
1304
|
-
|
|
1305
|
-
function applyHashRoute() {
|
|
1275
|
+
return !!mod && mod.type !== 'test' && mod.featureKeys?.includes(featureKey);
|
|
1276
|
+
});
|
|
1277
|
+
}
|
|
1278
|
+
|
|
1279
|
+
function getFileRowsWindow(items, renderKey) {
|
|
1280
|
+
if (fileRowsRenderKey !== renderKey) {
|
|
1281
|
+
fileRowsRenderKey = renderKey;
|
|
1282
|
+
fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
|
|
1283
|
+
}
|
|
1284
|
+
const visibleRows = items.slice(0, fileRowsRenderLimit);
|
|
1285
|
+
const hiddenRows = Math.max(0, items.length - visibleRows.length);
|
|
1286
|
+
return { visibleRows, hiddenRows, hasMoreRows: hiddenRows > 0 };
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function increaseFileRowsLimit() {
|
|
1290
|
+
fileRowsRenderLimit += FILE_ROWS_LIMIT_STEP;
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function bindFileRowsClick(container) {
|
|
1294
|
+
const fileRows = container.querySelector('#fileRows');
|
|
1295
|
+
if (!fileRows) return;
|
|
1296
|
+
fileRows.onclick = (event) => {
|
|
1297
|
+
const row = event.target.closest('.file-row[data-id]');
|
|
1298
|
+
if (!row) return;
|
|
1299
|
+
const moduleId = row.dataset.id;
|
|
1300
|
+
const mod = D.modules.find((m) => String(m.id) === moduleId);
|
|
1301
|
+
if (mod) openModulePanel(mod);
|
|
1302
|
+
};
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function applyHashRoute() {
|
|
1306
1306
|
const hash = (window.location.hash || '').replace(/^#/, '');
|
|
1307
1307
|
if (!hash) {
|
|
1308
1308
|
if (view === 'features' && drillFeatureKey) {
|
|
@@ -1350,148 +1350,148 @@ async function runAllTests() {
|
|
|
1350
1350
|
}
|
|
1351
1351
|
|
|
1352
1352
|
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
1353
|
-
let agentRunning = false;
|
|
1354
|
-
const agentRunningPaths = new Set(); // paths of files currently being processed by agent
|
|
1355
|
-
const agentQueuedPaths = new Set(); // paths of files waiting in queue
|
|
1356
|
-
let agentQueueState = [];
|
|
1357
|
-
let agentRunsState = [];
|
|
1358
|
-
let agentActiveRun = null;
|
|
1359
|
-
let currentRunId = null;
|
|
1360
|
-
let lastRunSummary = null;
|
|
1361
|
-
|
|
1362
|
-
// ─── Console Sessions ─────────────────────────────────────────────────────────
|
|
1363
|
-
const consoleSessions = []; // { id, title, lines, status, startTime }
|
|
1364
|
-
let activeSessionId = null; // currently viewed tab
|
|
1365
|
-
let runningSessionId = null; // tab that is currently receiving output
|
|
1366
|
-
const runSessionMap = new Map(); // runId -> sessionId
|
|
1367
|
-
const sessionRunMap = new Map(); // sessionId -> runId
|
|
1368
|
-
const SESSION_MAX = 25;
|
|
1369
|
-
const SESSION_LINE_LIMIT = 3000;
|
|
1370
|
-
const SESSIONS_KEY = 'viberadar_sessions';
|
|
1371
|
-
let terminalSearchQuery = '';
|
|
1372
|
-
let terminalSearchErrorsOnly = false;
|
|
1373
|
-
let terminalSearchCurrentRunOnly = false;
|
|
1374
|
-
let terminalSearchRegex = false;
|
|
1375
|
-
let terminalMatchRefs = [];
|
|
1376
|
-
let terminalCommandRefs = [];
|
|
1377
|
-
let terminalErrorRefs = [];
|
|
1378
|
-
let terminalMatchCursor = -1;
|
|
1379
|
-
let terminalCommandCursor = -1;
|
|
1380
|
-
let terminalErrorCursor = -1;
|
|
1353
|
+
let agentRunning = false;
|
|
1354
|
+
const agentRunningPaths = new Set(); // paths of files currently being processed by agent
|
|
1355
|
+
const agentQueuedPaths = new Set(); // paths of files waiting in queue
|
|
1356
|
+
let agentQueueState = [];
|
|
1357
|
+
let agentRunsState = [];
|
|
1358
|
+
let agentActiveRun = null;
|
|
1359
|
+
let currentRunId = null;
|
|
1360
|
+
let lastRunSummary = null;
|
|
1361
|
+
|
|
1362
|
+
// ─── Console Sessions ─────────────────────────────────────────────────────────
|
|
1363
|
+
const consoleSessions = []; // { id, title, lines, status, startTime }
|
|
1364
|
+
let activeSessionId = null; // currently viewed tab
|
|
1365
|
+
let runningSessionId = null; // tab that is currently receiving output
|
|
1366
|
+
const runSessionMap = new Map(); // runId -> sessionId
|
|
1367
|
+
const sessionRunMap = new Map(); // sessionId -> runId
|
|
1368
|
+
const SESSION_MAX = 25;
|
|
1369
|
+
const SESSION_LINE_LIMIT = 3000;
|
|
1370
|
+
const SESSIONS_KEY = 'viberadar_sessions';
|
|
1371
|
+
let terminalSearchQuery = '';
|
|
1372
|
+
let terminalSearchErrorsOnly = false;
|
|
1373
|
+
let terminalSearchCurrentRunOnly = false;
|
|
1374
|
+
let terminalSearchRegex = false;
|
|
1375
|
+
let terminalMatchRefs = [];
|
|
1376
|
+
let terminalCommandRefs = [];
|
|
1377
|
+
let terminalErrorRefs = [];
|
|
1378
|
+
let terminalMatchCursor = -1;
|
|
1379
|
+
let terminalCommandCursor = -1;
|
|
1380
|
+
let terminalErrorCursor = -1;
|
|
1381
1381
|
|
|
1382
1382
|
function _sessionId() {
|
|
1383
1383
|
return 's' + Date.now().toString(36) + Math.random().toString(36).slice(2, 5);
|
|
1384
1384
|
}
|
|
1385
1385
|
|
|
1386
|
-
function saveSessions() {
|
|
1387
|
-
try {
|
|
1388
|
-
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
1389
|
-
...s,
|
|
1390
|
-
runId: sessionRunMap.get(s.id) || s.runId || null,
|
|
1391
|
-
lines: s.lines.slice(-500)
|
|
1392
|
-
}));
|
|
1393
|
-
localStorage.setItem(SESSIONS_KEY, JSON.stringify(data));
|
|
1394
|
-
} catch {}
|
|
1395
|
-
}
|
|
1386
|
+
function saveSessions() {
|
|
1387
|
+
try {
|
|
1388
|
+
const data = consoleSessions.slice(-SESSION_MAX).map(s => ({
|
|
1389
|
+
...s,
|
|
1390
|
+
runId: sessionRunMap.get(s.id) || s.runId || null,
|
|
1391
|
+
lines: s.lines.slice(-500)
|
|
1392
|
+
}));
|
|
1393
|
+
localStorage.setItem(SESSIONS_KEY, JSON.stringify(data));
|
|
1394
|
+
} catch {}
|
|
1395
|
+
}
|
|
1396
1396
|
|
|
1397
1397
|
function restoreSessions() {
|
|
1398
1398
|
try {
|
|
1399
|
-
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
1400
|
-
if (!raw) return;
|
|
1401
|
-
const saved = JSON.parse(raw);
|
|
1402
|
-
consoleSessions.push(...saved);
|
|
1403
|
-
for (const s of consoleSessions) {
|
|
1404
|
-
if (s.runId) {
|
|
1405
|
-
runSessionMap.set(s.runId, s.id);
|
|
1406
|
-
sessionRunMap.set(s.id, s.runId);
|
|
1407
|
-
}
|
|
1408
|
-
}
|
|
1409
|
-
if (consoleSessions.length > 0) {
|
|
1410
|
-
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
1411
|
-
renderTabs();
|
|
1412
|
-
renderActiveSession();
|
|
1399
|
+
const raw = localStorage.getItem(SESSIONS_KEY);
|
|
1400
|
+
if (!raw) return;
|
|
1401
|
+
const saved = JSON.parse(raw);
|
|
1402
|
+
consoleSessions.push(...saved);
|
|
1403
|
+
for (const s of consoleSessions) {
|
|
1404
|
+
if (s.runId) {
|
|
1405
|
+
runSessionMap.set(s.runId, s.id);
|
|
1406
|
+
sessionRunMap.set(s.id, s.runId);
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
if (consoleSessions.length > 0) {
|
|
1410
|
+
activeSessionId = consoleSessions[consoleSessions.length - 1].id;
|
|
1411
|
+
renderTabs();
|
|
1412
|
+
renderActiveSession();
|
|
1413
1413
|
}
|
|
1414
1414
|
} catch {}
|
|
1415
1415
|
}
|
|
1416
1416
|
|
|
1417
|
-
function createSession(title, status = 'running', runId = null) {
|
|
1418
|
-
if (consoleSessions.length >= SESSION_MAX) {
|
|
1419
|
-
const dropped = consoleSessions.shift();
|
|
1420
|
-
if (dropped?.id) {
|
|
1421
|
-
const droppedRunId = sessionRunMap.get(dropped.id);
|
|
1422
|
-
if (droppedRunId) runSessionMap.delete(droppedRunId);
|
|
1423
|
-
sessionRunMap.delete(dropped.id);
|
|
1424
|
-
}
|
|
1425
|
-
}
|
|
1426
|
-
const s = { id: _sessionId(), title, lines: [], status, startTime: Date.now(), runId };
|
|
1427
|
-
consoleSessions.push(s);
|
|
1428
|
-
activeSessionId = s.id;
|
|
1429
|
-
if (runId) {
|
|
1430
|
-
runSessionMap.set(runId, s.id);
|
|
1431
|
-
sessionRunMap.set(s.id, runId);
|
|
1432
|
-
}
|
|
1433
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
1434
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
1435
|
-
renderTabs();
|
|
1436
|
-
renderActiveSession();
|
|
1437
|
-
saveSessions();
|
|
1417
|
+
function createSession(title, status = 'running', runId = null) {
|
|
1418
|
+
if (consoleSessions.length >= SESSION_MAX) {
|
|
1419
|
+
const dropped = consoleSessions.shift();
|
|
1420
|
+
if (dropped?.id) {
|
|
1421
|
+
const droppedRunId = sessionRunMap.get(dropped.id);
|
|
1422
|
+
if (droppedRunId) runSessionMap.delete(droppedRunId);
|
|
1423
|
+
sessionRunMap.delete(dropped.id);
|
|
1424
|
+
}
|
|
1425
|
+
}
|
|
1426
|
+
const s = { id: _sessionId(), title, lines: [], status, startTime: Date.now(), runId };
|
|
1427
|
+
consoleSessions.push(s);
|
|
1428
|
+
activeSessionId = s.id;
|
|
1429
|
+
if (runId) {
|
|
1430
|
+
runSessionMap.set(runId, s.id);
|
|
1431
|
+
sessionRunMap.set(s.id, runId);
|
|
1432
|
+
}
|
|
1433
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
1434
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
1435
|
+
renderTabs();
|
|
1436
|
+
renderActiveSession();
|
|
1437
|
+
saveSessions();
|
|
1438
1438
|
return s.id;
|
|
1439
1439
|
}
|
|
1440
1440
|
|
|
1441
|
-
function switchSession(id) {
|
|
1442
|
-
activeSessionId = id;
|
|
1443
|
-
currentRunId = sessionRunMap.get(id) || null;
|
|
1444
|
-
const s = consoleSessions.find(s => s.id === id);
|
|
1445
|
-
if (s) {
|
|
1446
|
-
const statusText = s.status === 'running' ? 'работает…'
|
|
1447
|
-
: s.status === 'ok' ? '✅ готово'
|
|
1448
|
-
: s.status === 'error' ? '❌ ошибка'
|
|
1441
|
+
function switchSession(id) {
|
|
1442
|
+
activeSessionId = id;
|
|
1443
|
+
currentRunId = sessionRunMap.get(id) || null;
|
|
1444
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
1445
|
+
if (s) {
|
|
1446
|
+
const statusText = s.status === 'running' ? 'работает…'
|
|
1447
|
+
: s.status === 'ok' ? '✅ готово'
|
|
1448
|
+
: s.status === 'error' ? '❌ ошибка'
|
|
1449
1449
|
: '';
|
|
1450
1450
|
document.getElementById('agentPanelTitle').textContent = s.title;
|
|
1451
1451
|
document.getElementById('agentPanelStatus').textContent = statusText;
|
|
1452
|
-
}
|
|
1453
|
-
renderTabs();
|
|
1454
|
-
renderActiveSession();
|
|
1455
|
-
syncSummaryForActiveSession();
|
|
1456
|
-
}
|
|
1457
|
-
|
|
1458
|
-
function closeSession(id) {
|
|
1459
|
-
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
1460
|
-
if (idx === -1) return;
|
|
1461
|
-
const runId = sessionRunMap.get(id);
|
|
1462
|
-
if (runId) {
|
|
1463
|
-
sessionRunMap.delete(id);
|
|
1464
|
-
runSessionMap.delete(runId);
|
|
1465
|
-
}
|
|
1466
|
-
consoleSessions.splice(idx, 1);
|
|
1467
|
-
if (activeSessionId === id) {
|
|
1468
|
-
activeSessionId = consoleSessions.length > 0
|
|
1469
|
-
? consoleSessions[Math.min(idx, consoleSessions.length - 1)].id
|
|
1470
|
-
: null;
|
|
1471
|
-
}
|
|
1472
|
-
renderTabs();
|
|
1473
|
-
renderActiveSession();
|
|
1474
|
-
syncSummaryForActiveSession();
|
|
1475
|
-
saveSessions();
|
|
1476
|
-
}
|
|
1477
|
-
|
|
1478
|
-
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
1479
|
-
const s = consoleSessions.find(s => s.id === id);
|
|
1480
|
-
if (!s) return;
|
|
1452
|
+
}
|
|
1453
|
+
renderTabs();
|
|
1454
|
+
renderActiveSession();
|
|
1455
|
+
syncSummaryForActiveSession();
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
function closeSession(id) {
|
|
1459
|
+
const idx = consoleSessions.findIndex(s => s.id === id);
|
|
1460
|
+
if (idx === -1) return;
|
|
1461
|
+
const runId = sessionRunMap.get(id);
|
|
1462
|
+
if (runId) {
|
|
1463
|
+
sessionRunMap.delete(id);
|
|
1464
|
+
runSessionMap.delete(runId);
|
|
1465
|
+
}
|
|
1466
|
+
consoleSessions.splice(idx, 1);
|
|
1467
|
+
if (activeSessionId === id) {
|
|
1468
|
+
activeSessionId = consoleSessions.length > 0
|
|
1469
|
+
? consoleSessions[Math.min(idx, consoleSessions.length - 1)].id
|
|
1470
|
+
: null;
|
|
1471
|
+
}
|
|
1472
|
+
renderTabs();
|
|
1473
|
+
renderActiveSession();
|
|
1474
|
+
syncSummaryForActiveSession();
|
|
1475
|
+
saveSessions();
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
function appendToSession(id, lineOrNode, isError = false, isDim = false) {
|
|
1479
|
+
const s = consoleSessions.find(s => s.id === id);
|
|
1480
|
+
if (!s) return;
|
|
1481
1481
|
let stored;
|
|
1482
1482
|
if (typeof lineOrNode === 'string') {
|
|
1483
1483
|
stored = { text: lineOrNode, isError, isDim };
|
|
1484
1484
|
} else {
|
|
1485
1485
|
stored = { html: lineOrNode.outerHTML };
|
|
1486
|
-
}
|
|
1487
|
-
s.lines.push(stored);
|
|
1488
|
-
if (s.lines.length > SESSION_LINE_LIMIT) {
|
|
1489
|
-
s.lines.splice(0, s.lines.length - SESSION_LINE_LIMIT);
|
|
1490
|
-
}
|
|
1491
|
-
if (activeSessionId === id) {
|
|
1492
|
-
renderActiveSession();
|
|
1493
|
-
}
|
|
1494
|
-
}
|
|
1486
|
+
}
|
|
1487
|
+
s.lines.push(stored);
|
|
1488
|
+
if (s.lines.length > SESSION_LINE_LIMIT) {
|
|
1489
|
+
s.lines.splice(0, s.lines.length - SESSION_LINE_LIMIT);
|
|
1490
|
+
}
|
|
1491
|
+
if (activeSessionId === id) {
|
|
1492
|
+
renderActiveSession();
|
|
1493
|
+
}
|
|
1494
|
+
}
|
|
1495
1495
|
|
|
1496
1496
|
function updateSessionStatus(id, status) {
|
|
1497
1497
|
const s = consoleSessions.find(s => s.id === id);
|
|
@@ -1519,9 +1519,9 @@ function renderTabs() {
|
|
|
1519
1519
|
if (active) active.scrollIntoView({ block: 'nearest', inline: 'nearest' });
|
|
1520
1520
|
}
|
|
1521
1521
|
|
|
1522
|
-
function copyTerminalContent() {
|
|
1523
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1524
|
-
if (!s || s.lines.length === 0) return;
|
|
1522
|
+
function copyTerminalContent() {
|
|
1523
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1524
|
+
if (!s || s.lines.length === 0) return;
|
|
1525
1525
|
// Extract plain text from each line (strip HTML for rich nodes)
|
|
1526
1526
|
const text = s.lines.map(l => {
|
|
1527
1527
|
if (l.text !== undefined) return l.text;
|
|
@@ -1539,214 +1539,214 @@ function copyTerminalContent() {
|
|
|
1539
1539
|
btn.textContent = '✓';
|
|
1540
1540
|
btn.classList.add('copied');
|
|
1541
1541
|
setTimeout(() => { btn.textContent = prev; btn.classList.remove('copied'); }, 1500);
|
|
1542
|
-
});
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
function downloadTextArtifact(filename, content) {
|
|
1546
|
-
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
|
1547
|
-
const a = document.createElement('a');
|
|
1548
|
-
a.href = URL.createObjectURL(blob);
|
|
1549
|
-
a.download = filename;
|
|
1550
|
-
document.body.appendChild(a);
|
|
1551
|
-
a.click();
|
|
1552
|
-
setTimeout(() => {
|
|
1553
|
-
URL.revokeObjectURL(a.href);
|
|
1554
|
-
a.remove();
|
|
1555
|
-
}, 100);
|
|
1556
|
-
}
|
|
1557
|
-
|
|
1558
|
-
function getActiveRunForExport() {
|
|
1559
|
-
const runId = sessionRunMap.get(activeSessionId) || currentRunId;
|
|
1560
|
-
if (runId) {
|
|
1561
|
-
const fromState = (agentRunsState || []).find((r) => r.runId === runId);
|
|
1562
|
-
if (fromState) return fromState;
|
|
1563
|
-
}
|
|
1564
|
-
return null;
|
|
1565
|
-
}
|
|
1566
|
-
|
|
1567
|
-
function exportActiveRun() {
|
|
1568
|
-
const run = getActiveRunForExport();
|
|
1569
|
-
const session = consoleSessions.find((s) => s.id === activeSessionId);
|
|
1570
|
-
if (!run && !session) {
|
|
1571
|
-
flashToolbarHint('Нечего экспортировать');
|
|
1572
|
-
return;
|
|
1573
|
-
}
|
|
1574
|
-
const mode = window.prompt('Формат экспорта: md или json', 'md');
|
|
1575
|
-
const format = (mode || 'md').trim().toLowerCase() === 'json' ? 'json' : 'md';
|
|
1576
|
-
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1577
|
-
const runId = run?.runId || sessionRunMap.get(activeSessionId) || 'session';
|
|
1578
|
-
if (format === 'json') {
|
|
1579
|
-
const payload = {
|
|
1580
|
-
run,
|
|
1581
|
-
sessionTitle: session?.title || null,
|
|
1582
|
-
lines: (session?.lines || []).map((line) => extractLineText(line)),
|
|
1583
|
-
exportedAt: new Date().toISOString(),
|
|
1584
|
-
};
|
|
1585
|
-
downloadTextArtifact(`viberadar-run-${runId}-${ts}.json`, JSON.stringify(payload, null, 2));
|
|
1586
|
-
return;
|
|
1587
|
-
}
|
|
1588
|
-
const lines = (session?.lines || []).map((line) => extractLineText(line)).filter(Boolean);
|
|
1589
|
-
const outcomes = Array.isArray(run?.fileOutcomes) ? run.fileOutcomes : [];
|
|
1590
|
-
const stats = run?.validationStats || {};
|
|
1591
|
-
const md = [
|
|
1592
|
-
`# VibeRadar Run Export`,
|
|
1593
|
-
``,
|
|
1594
|
-
`- runId: ${run?.runId || runId}`,
|
|
1595
|
-
`- title: ${run?.title || session?.title || '-'}`,
|
|
1596
|
-
`- phase: ${run?.phase || '-'}`,
|
|
1597
|
-
`- exportedAt: ${new Date().toISOString()}`,
|
|
1598
|
-
``,
|
|
1599
|
-
`## Validation`,
|
|
1600
|
-
``,
|
|
1601
|
-
`- covered: ${stats.covered ?? 0}`,
|
|
1602
|
-
`- not-covered: ${stats.notCovered ?? 0}`,
|
|
1603
|
-
`- blocked: ${stats.blocked ?? 0}`,
|
|
1604
|
-
`- infra: ${stats.infra ?? 0}`,
|
|
1605
|
-
``,
|
|
1606
|
-
`## File Outcomes`,
|
|
1607
|
-
``,
|
|
1608
|
-
...outcomes.map((o) => `- ${o.status}: ${o.sourcePath}${o.testFile ? ` -> ${o.testFile}` : ''}${o.reason ? ` (${o.reason})` : ''}`),
|
|
1609
|
-
``,
|
|
1610
|
-
`## Logs`,
|
|
1611
|
-
``,
|
|
1612
|
-
'```text',
|
|
1613
|
-
...lines,
|
|
1614
|
-
'```',
|
|
1615
|
-
].join('\n');
|
|
1616
|
-
downloadTextArtifact(`viberadar-run-${runId}-${ts}.md`, md);
|
|
1617
|
-
}
|
|
1618
|
-
|
|
1619
|
-
function renderActiveSession() {
|
|
1620
|
-
const term = document.getElementById('agentTerminal');
|
|
1621
|
-
if (!term) return;
|
|
1622
|
-
term.innerHTML = '';
|
|
1623
|
-
terminalMatchRefs = [];
|
|
1624
|
-
terminalCommandRefs = [];
|
|
1625
|
-
terminalErrorRefs = [];
|
|
1626
|
-
terminalMatchCursor = -1;
|
|
1627
|
-
terminalCommandCursor = -1;
|
|
1628
|
-
terminalErrorCursor = -1;
|
|
1629
|
-
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1630
|
-
if (!s) {
|
|
1631
|
-
document.getElementById('agentSearchMeta').textContent = '0 matches';
|
|
1632
|
-
updateToolbarButtonsState();
|
|
1633
|
-
return;
|
|
1634
|
-
}
|
|
1635
|
-
const normalizedQuery = (terminalSearchQuery || '').trim();
|
|
1636
|
-
let regex = null;
|
|
1637
|
-
if (normalizedQuery && terminalSearchRegex) {
|
|
1638
|
-
try { regex = new RegExp(normalizedQuery, 'i'); } catch { regex = null; }
|
|
1639
|
-
}
|
|
1640
|
-
for (let i = 0; i < s.lines.length; i++) {
|
|
1641
|
-
const ln = s.lines[i];
|
|
1642
|
-
const text = extractLineText(ln);
|
|
1643
|
-
const isErrorLine = !!ln.isError || /(^|\s)❌/.test(text) || /\berror\b/i.test(text);
|
|
1644
|
-
const isCommandLine = /^\s*⚡\s*\$/.test(text);
|
|
1645
|
-
if (terminalSearchCurrentRunOnly && currentRunId && s.runId && s.runId !== currentRunId) continue;
|
|
1646
|
-
if (terminalSearchErrorsOnly && !isErrorLine) continue;
|
|
1647
|
-
let isMatch = false;
|
|
1648
|
-
if (!normalizedQuery) {
|
|
1649
|
-
isMatch = false;
|
|
1650
|
-
} else if (regex) {
|
|
1651
|
-
isMatch = regex.test(text);
|
|
1652
|
-
} else {
|
|
1653
|
-
isMatch = text.toLowerCase().includes(normalizedQuery.toLowerCase());
|
|
1654
|
-
}
|
|
1655
|
-
if (normalizedQuery && !isMatch) continue;
|
|
1656
|
-
|
|
1657
|
-
const el = document.createElement('div');
|
|
1658
|
-
if (ln.html) {
|
|
1659
|
-
el.innerHTML = ln.html;
|
|
1660
|
-
} else {
|
|
1661
|
-
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1662
|
-
el.textContent = ln.text;
|
|
1663
|
-
}
|
|
1664
|
-
if (isCommandLine) el.classList.add('command');
|
|
1665
|
-
if (isMatch) el.classList.add('match');
|
|
1666
|
-
el.dataset.lineIndex = String(i);
|
|
1667
|
-
term.appendChild(el);
|
|
1668
|
-
if (isMatch) terminalMatchRefs.push(el);
|
|
1669
|
-
if (isCommandLine) terminalCommandRefs.push(el);
|
|
1670
|
-
if (isErrorLine) terminalErrorRefs.push(el);
|
|
1671
|
-
}
|
|
1672
|
-
term.scrollTop = term.scrollHeight;
|
|
1673
|
-
document.getElementById('agentSearchMeta').textContent =
|
|
1674
|
-
`${terminalMatchRefs.length} matches • ${terminalCommandRefs.length} commands • ${terminalErrorRefs.length} errors`;
|
|
1675
|
-
updateToolbarButtonsState();
|
|
1676
|
-
}
|
|
1677
|
-
|
|
1678
|
-
function updateToolbarButtonsState() {
|
|
1679
|
-
const setState = (id, enabled) => {
|
|
1680
|
-
const el = document.getElementById(id);
|
|
1681
|
-
if (!el) return;
|
|
1682
|
-
el.disabled = !enabled;
|
|
1683
|
-
};
|
|
1684
|
-
setState('btnMatchPrev', terminalMatchRefs.length > 0);
|
|
1685
|
-
setState('btnMatchNext', terminalMatchRefs.length > 0);
|
|
1686
|
-
setState('btnCmdPrev', terminalCommandRefs.length > 0);
|
|
1687
|
-
setState('btnCmdNext', terminalCommandRefs.length > 0);
|
|
1688
|
-
setState('btnErrPrev', terminalErrorRefs.length > 0);
|
|
1689
|
-
setState('btnErrNext', terminalErrorRefs.length > 0);
|
|
1690
|
-
const hasExport = !!getActiveRunForExport() || !!consoleSessions.find((s) => s.id === activeSessionId);
|
|
1691
|
-
setState('btnExportRun', hasExport);
|
|
1692
|
-
}
|
|
1693
|
-
|
|
1694
|
-
function flashToolbarHint(text) {
|
|
1695
|
-
const meta = document.getElementById('agentSearchMeta');
|
|
1696
|
-
if (!meta) return;
|
|
1697
|
-
const previous = meta.textContent;
|
|
1698
|
-
meta.textContent = text;
|
|
1699
|
-
meta.style.color = 'var(--yellow)';
|
|
1700
|
-
setTimeout(() => {
|
|
1701
|
-
meta.style.color = '';
|
|
1702
|
-
if (meta.textContent === text) {
|
|
1703
|
-
meta.textContent = previous || '0 matches';
|
|
1704
|
-
}
|
|
1705
|
-
}, 1400);
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
function extractLineText(line) {
|
|
1709
|
-
if (!line) return '';
|
|
1710
|
-
if (line.text !== undefined) return String(line.text || '');
|
|
1711
|
-
if (line.html) {
|
|
1712
|
-
const tmp = document.createElement('div');
|
|
1713
|
-
tmp.innerHTML = line.html;
|
|
1714
|
-
return tmp.innerText || '';
|
|
1715
|
-
}
|
|
1716
|
-
return '';
|
|
1717
|
-
}
|
|
1718
|
-
|
|
1719
|
-
function jumpByRefs(refs, dir, cursorName) {
|
|
1720
|
-
if (!refs.length) {
|
|
1721
|
-
const label = cursorName === 'match' ? 'совпадений' : cursorName === 'command' ? 'команд' : 'ошибок';
|
|
1722
|
-
flashToolbarHint(`Нет ${label} для навигации`);
|
|
1723
|
-
return;
|
|
1724
|
-
}
|
|
1725
|
-
if (cursorName === 'match') {
|
|
1726
|
-
terminalMatchCursor = (terminalMatchCursor + dir + refs.length) % refs.length;
|
|
1727
|
-
refs[terminalMatchCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
if (cursorName === 'command') {
|
|
1731
|
-
terminalCommandCursor = (terminalCommandCursor + dir + refs.length) % refs.length;
|
|
1732
|
-
refs[terminalCommandCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1733
|
-
return;
|
|
1734
|
-
}
|
|
1735
|
-
terminalErrorCursor = (terminalErrorCursor + dir + refs.length) % refs.length;
|
|
1736
|
-
refs[terminalErrorCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function jumpTerminalMatch(dir) {
|
|
1740
|
-
jumpByRefs(terminalMatchRefs, dir > 0 ? 1 : -1, 'match');
|
|
1741
|
-
}
|
|
1742
|
-
|
|
1743
|
-
function jumpCommand(dir) {
|
|
1744
|
-
jumpByRefs(terminalCommandRefs, dir > 0 ? 1 : -1, 'command');
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
function jumpError(dir) {
|
|
1748
|
-
jumpByRefs(terminalErrorRefs, dir > 0 ? 1 : -1, 'error');
|
|
1749
|
-
}
|
|
1542
|
+
});
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
function downloadTextArtifact(filename, content) {
|
|
1546
|
+
const blob = new Blob([content], { type: 'text/plain;charset=utf-8' });
|
|
1547
|
+
const a = document.createElement('a');
|
|
1548
|
+
a.href = URL.createObjectURL(blob);
|
|
1549
|
+
a.download = filename;
|
|
1550
|
+
document.body.appendChild(a);
|
|
1551
|
+
a.click();
|
|
1552
|
+
setTimeout(() => {
|
|
1553
|
+
URL.revokeObjectURL(a.href);
|
|
1554
|
+
a.remove();
|
|
1555
|
+
}, 100);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
function getActiveRunForExport() {
|
|
1559
|
+
const runId = sessionRunMap.get(activeSessionId) || currentRunId;
|
|
1560
|
+
if (runId) {
|
|
1561
|
+
const fromState = (agentRunsState || []).find((r) => r.runId === runId);
|
|
1562
|
+
if (fromState) return fromState;
|
|
1563
|
+
}
|
|
1564
|
+
return null;
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
function exportActiveRun() {
|
|
1568
|
+
const run = getActiveRunForExport();
|
|
1569
|
+
const session = consoleSessions.find((s) => s.id === activeSessionId);
|
|
1570
|
+
if (!run && !session) {
|
|
1571
|
+
flashToolbarHint('Нечего экспортировать');
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
const mode = window.prompt('Формат экспорта: md или json', 'md');
|
|
1575
|
+
const format = (mode || 'md').trim().toLowerCase() === 'json' ? 'json' : 'md';
|
|
1576
|
+
const ts = new Date().toISOString().replace(/[:.]/g, '-');
|
|
1577
|
+
const runId = run?.runId || sessionRunMap.get(activeSessionId) || 'session';
|
|
1578
|
+
if (format === 'json') {
|
|
1579
|
+
const payload = {
|
|
1580
|
+
run,
|
|
1581
|
+
sessionTitle: session?.title || null,
|
|
1582
|
+
lines: (session?.lines || []).map((line) => extractLineText(line)),
|
|
1583
|
+
exportedAt: new Date().toISOString(),
|
|
1584
|
+
};
|
|
1585
|
+
downloadTextArtifact(`viberadar-run-${runId}-${ts}.json`, JSON.stringify(payload, null, 2));
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
const lines = (session?.lines || []).map((line) => extractLineText(line)).filter(Boolean);
|
|
1589
|
+
const outcomes = Array.isArray(run?.fileOutcomes) ? run.fileOutcomes : [];
|
|
1590
|
+
const stats = run?.validationStats || {};
|
|
1591
|
+
const md = [
|
|
1592
|
+
`# VibeRadar Run Export`,
|
|
1593
|
+
``,
|
|
1594
|
+
`- runId: ${run?.runId || runId}`,
|
|
1595
|
+
`- title: ${run?.title || session?.title || '-'}`,
|
|
1596
|
+
`- phase: ${run?.phase || '-'}`,
|
|
1597
|
+
`- exportedAt: ${new Date().toISOString()}`,
|
|
1598
|
+
``,
|
|
1599
|
+
`## Validation`,
|
|
1600
|
+
``,
|
|
1601
|
+
`- covered: ${stats.covered ?? 0}`,
|
|
1602
|
+
`- not-covered: ${stats.notCovered ?? 0}`,
|
|
1603
|
+
`- blocked: ${stats.blocked ?? 0}`,
|
|
1604
|
+
`- infra: ${stats.infra ?? 0}`,
|
|
1605
|
+
``,
|
|
1606
|
+
`## File Outcomes`,
|
|
1607
|
+
``,
|
|
1608
|
+
...outcomes.map((o) => `- ${o.status}: ${o.sourcePath}${o.testFile ? ` -> ${o.testFile}` : ''}${o.reason ? ` (${o.reason})` : ''}`),
|
|
1609
|
+
``,
|
|
1610
|
+
`## Logs`,
|
|
1611
|
+
``,
|
|
1612
|
+
'```text',
|
|
1613
|
+
...lines,
|
|
1614
|
+
'```',
|
|
1615
|
+
].join('\n');
|
|
1616
|
+
downloadTextArtifact(`viberadar-run-${runId}-${ts}.md`, md);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
function renderActiveSession() {
|
|
1620
|
+
const term = document.getElementById('agentTerminal');
|
|
1621
|
+
if (!term) return;
|
|
1622
|
+
term.innerHTML = '';
|
|
1623
|
+
terminalMatchRefs = [];
|
|
1624
|
+
terminalCommandRefs = [];
|
|
1625
|
+
terminalErrorRefs = [];
|
|
1626
|
+
terminalMatchCursor = -1;
|
|
1627
|
+
terminalCommandCursor = -1;
|
|
1628
|
+
terminalErrorCursor = -1;
|
|
1629
|
+
const s = consoleSessions.find(s => s.id === activeSessionId);
|
|
1630
|
+
if (!s) {
|
|
1631
|
+
document.getElementById('agentSearchMeta').textContent = '0 matches';
|
|
1632
|
+
updateToolbarButtonsState();
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
const normalizedQuery = (terminalSearchQuery || '').trim();
|
|
1636
|
+
let regex = null;
|
|
1637
|
+
if (normalizedQuery && terminalSearchRegex) {
|
|
1638
|
+
try { regex = new RegExp(normalizedQuery, 'i'); } catch { regex = null; }
|
|
1639
|
+
}
|
|
1640
|
+
for (let i = 0; i < s.lines.length; i++) {
|
|
1641
|
+
const ln = s.lines[i];
|
|
1642
|
+
const text = extractLineText(ln);
|
|
1643
|
+
const isErrorLine = !!ln.isError || /(^|\s)❌/.test(text) || /\berror\b/i.test(text);
|
|
1644
|
+
const isCommandLine = /^\s*⚡\s*\$/.test(text);
|
|
1645
|
+
if (terminalSearchCurrentRunOnly && currentRunId && s.runId && s.runId !== currentRunId) continue;
|
|
1646
|
+
if (terminalSearchErrorsOnly && !isErrorLine) continue;
|
|
1647
|
+
let isMatch = false;
|
|
1648
|
+
if (!normalizedQuery) {
|
|
1649
|
+
isMatch = false;
|
|
1650
|
+
} else if (regex) {
|
|
1651
|
+
isMatch = regex.test(text);
|
|
1652
|
+
} else {
|
|
1653
|
+
isMatch = text.toLowerCase().includes(normalizedQuery.toLowerCase());
|
|
1654
|
+
}
|
|
1655
|
+
if (normalizedQuery && !isMatch) continue;
|
|
1656
|
+
|
|
1657
|
+
const el = document.createElement('div');
|
|
1658
|
+
if (ln.html) {
|
|
1659
|
+
el.innerHTML = ln.html;
|
|
1660
|
+
} else {
|
|
1661
|
+
el.className = 'agent-line' + (ln.isError ? ' err' : ln.isDim ? ' dim' : '');
|
|
1662
|
+
el.textContent = ln.text;
|
|
1663
|
+
}
|
|
1664
|
+
if (isCommandLine) el.classList.add('command');
|
|
1665
|
+
if (isMatch) el.classList.add('match');
|
|
1666
|
+
el.dataset.lineIndex = String(i);
|
|
1667
|
+
term.appendChild(el);
|
|
1668
|
+
if (isMatch) terminalMatchRefs.push(el);
|
|
1669
|
+
if (isCommandLine) terminalCommandRefs.push(el);
|
|
1670
|
+
if (isErrorLine) terminalErrorRefs.push(el);
|
|
1671
|
+
}
|
|
1672
|
+
term.scrollTop = term.scrollHeight;
|
|
1673
|
+
document.getElementById('agentSearchMeta').textContent =
|
|
1674
|
+
`${terminalMatchRefs.length} matches • ${terminalCommandRefs.length} commands • ${terminalErrorRefs.length} errors`;
|
|
1675
|
+
updateToolbarButtonsState();
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
function updateToolbarButtonsState() {
|
|
1679
|
+
const setState = (id, enabled) => {
|
|
1680
|
+
const el = document.getElementById(id);
|
|
1681
|
+
if (!el) return;
|
|
1682
|
+
el.disabled = !enabled;
|
|
1683
|
+
};
|
|
1684
|
+
setState('btnMatchPrev', terminalMatchRefs.length > 0);
|
|
1685
|
+
setState('btnMatchNext', terminalMatchRefs.length > 0);
|
|
1686
|
+
setState('btnCmdPrev', terminalCommandRefs.length > 0);
|
|
1687
|
+
setState('btnCmdNext', terminalCommandRefs.length > 0);
|
|
1688
|
+
setState('btnErrPrev', terminalErrorRefs.length > 0);
|
|
1689
|
+
setState('btnErrNext', terminalErrorRefs.length > 0);
|
|
1690
|
+
const hasExport = !!getActiveRunForExport() || !!consoleSessions.find((s) => s.id === activeSessionId);
|
|
1691
|
+
setState('btnExportRun', hasExport);
|
|
1692
|
+
}
|
|
1693
|
+
|
|
1694
|
+
function flashToolbarHint(text) {
|
|
1695
|
+
const meta = document.getElementById('agentSearchMeta');
|
|
1696
|
+
if (!meta) return;
|
|
1697
|
+
const previous = meta.textContent;
|
|
1698
|
+
meta.textContent = text;
|
|
1699
|
+
meta.style.color = 'var(--yellow)';
|
|
1700
|
+
setTimeout(() => {
|
|
1701
|
+
meta.style.color = '';
|
|
1702
|
+
if (meta.textContent === text) {
|
|
1703
|
+
meta.textContent = previous || '0 matches';
|
|
1704
|
+
}
|
|
1705
|
+
}, 1400);
|
|
1706
|
+
}
|
|
1707
|
+
|
|
1708
|
+
function extractLineText(line) {
|
|
1709
|
+
if (!line) return '';
|
|
1710
|
+
if (line.text !== undefined) return String(line.text || '');
|
|
1711
|
+
if (line.html) {
|
|
1712
|
+
const tmp = document.createElement('div');
|
|
1713
|
+
tmp.innerHTML = line.html;
|
|
1714
|
+
return tmp.innerText || '';
|
|
1715
|
+
}
|
|
1716
|
+
return '';
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function jumpByRefs(refs, dir, cursorName) {
|
|
1720
|
+
if (!refs.length) {
|
|
1721
|
+
const label = cursorName === 'match' ? 'совпадений' : cursorName === 'command' ? 'команд' : 'ошибок';
|
|
1722
|
+
flashToolbarHint(`Нет ${label} для навигации`);
|
|
1723
|
+
return;
|
|
1724
|
+
}
|
|
1725
|
+
if (cursorName === 'match') {
|
|
1726
|
+
terminalMatchCursor = (terminalMatchCursor + dir + refs.length) % refs.length;
|
|
1727
|
+
refs[terminalMatchCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1728
|
+
return;
|
|
1729
|
+
}
|
|
1730
|
+
if (cursorName === 'command') {
|
|
1731
|
+
terminalCommandCursor = (terminalCommandCursor + dir + refs.length) % refs.length;
|
|
1732
|
+
refs[terminalCommandCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1733
|
+
return;
|
|
1734
|
+
}
|
|
1735
|
+
terminalErrorCursor = (terminalErrorCursor + dir + refs.length) % refs.length;
|
|
1736
|
+
refs[terminalErrorCursor].scrollIntoView({ block: 'center', behavior: 'smooth' });
|
|
1737
|
+
}
|
|
1738
|
+
|
|
1739
|
+
function jumpTerminalMatch(dir) {
|
|
1740
|
+
jumpByRefs(terminalMatchRefs, dir > 0 ? 1 : -1, 'match');
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
function jumpCommand(dir) {
|
|
1744
|
+
jumpByRefs(terminalCommandRefs, dir > 0 ? 1 : -1, 'command');
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
function jumpError(dir) {
|
|
1748
|
+
jumpByRefs(terminalErrorRefs, dir > 0 ? 1 : -1, 'error');
|
|
1749
|
+
}
|
|
1750
1750
|
|
|
1751
1751
|
async function setAgent(agent) {
|
|
1752
1752
|
await fetch('/api/set-agent', {
|
|
@@ -1795,199 +1795,199 @@ function isFileAgentActive(relPath) {
|
|
|
1795
1795
|
return null;
|
|
1796
1796
|
}
|
|
1797
1797
|
|
|
1798
|
-
function updateQueueBadge(n) {
|
|
1799
|
-
document.getElementById('agentQueueCount').textContent = n;
|
|
1800
|
-
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1801
|
-
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1802
|
-
renderQueuePanel();
|
|
1803
|
-
}
|
|
1804
|
-
|
|
1805
|
-
function renderQueuePanel() {
|
|
1806
|
-
const panel = document.getElementById('agentQueuePanel');
|
|
1807
|
-
if (!panel) return;
|
|
1808
|
-
if (!Array.isArray(agentQueueState) || agentQueueState.length === 0) {
|
|
1809
|
-
panel.style.display = 'none';
|
|
1810
|
-
panel.innerHTML = '';
|
|
1811
|
-
return;
|
|
1812
|
-
}
|
|
1813
|
-
panel.style.display = 'block';
|
|
1814
|
-
panel.innerHTML = `
|
|
1815
|
-
<div class="agent-queue-title">Очередь задач (${agentQueueState.length})</div>
|
|
1816
|
-
${agentQueueState.map((item, idx) => `
|
|
1817
|
-
<div class="agent-queue-item">
|
|
1818
|
-
<span class="agent-queue-pos">#${item.position ?? idx + 1}</span>
|
|
1819
|
-
<span title="${escapeHtml(item.title || '')}">${escapeHtml(item.title || item.task || item.runId)}</span>
|
|
1820
|
-
<span style="color:var(--dim)">(${escapeHtml(item.task || 'task')})</span>
|
|
1821
|
-
<div class="agent-queue-actions">
|
|
1822
|
-
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','up')" title="Сдвинуть вверх">↑</button>
|
|
1823
|
-
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','down')" title="Сдвинуть вниз">↓</button>
|
|
1824
|
-
<button class="agent-queue-action" onclick="cancelQueueItem('${item.runId}')" title="Отменить задачу">✕</button>
|
|
1825
|
-
<button class="agent-queue-action" onclick="retryRun('${item.runId}')" title="Retry">↻</button>
|
|
1826
|
-
</div>
|
|
1827
|
-
</div>
|
|
1828
|
-
`).join('')}
|
|
1829
|
-
`;
|
|
1830
|
-
}
|
|
1831
|
-
|
|
1832
|
-
function renderRunSummaryMatrix(summary = lastRunSummary) {
|
|
1833
|
-
const box = document.getElementById('agentSummaryMatrix');
|
|
1834
|
-
if (!box) return;
|
|
1835
|
-
const outcomes = summary?.fileOutcomes || [];
|
|
1836
|
-
if (!Array.isArray(outcomes) || outcomes.length === 0) {
|
|
1837
|
-
box.style.display = 'none';
|
|
1838
|
-
box.innerHTML = '';
|
|
1839
|
-
return;
|
|
1840
|
-
}
|
|
1841
|
-
const stats = summary?.validationStats || {};
|
|
1842
|
-
box.style.display = 'block';
|
|
1843
|
-
box.innerHTML = `
|
|
1844
|
-
<div class="agent-summary-title">
|
|
1845
|
-
Матрица итогов (run: ${escapeHtml(summary?.runId || '—')}) • covered: ${stats.covered ?? 0} • not-covered: ${stats.notCovered ?? 0} • blocked: ${stats.blocked ?? 0} • infra: ${stats.infra ?? 0}
|
|
1846
|
-
</div>
|
|
1847
|
-
${outcomes.map((entry) => `
|
|
1848
|
-
<div class="agent-summary-row">
|
|
1849
|
-
<span class="agent-summary-status-${entry.status}">${escapeHtml(entry.status)}</span>
|
|
1850
|
-
<span title="${escapeHtml(entry.sourcePath || '')}">${escapeHtml(entry.sourcePath || '')}</span>
|
|
1851
|
-
<span style="color:var(--dim)">${escapeHtml(entry.testFile || entry.reason || '')}</span>
|
|
1852
|
-
</div>
|
|
1853
|
-
`).join('')}
|
|
1854
|
-
`;
|
|
1855
|
-
}
|
|
1856
|
-
|
|
1857
|
-
function findSummaryForRun(runId) {
|
|
1858
|
-
if (!runId) return null;
|
|
1859
|
-
return (agentRunsState || []).find((r) => r.runId === runId && Array.isArray(r.fileOutcomes) && r.fileOutcomes.length > 0) || null;
|
|
1860
|
-
}
|
|
1861
|
-
|
|
1862
|
-
function syncSummaryForActiveSession() {
|
|
1863
|
-
const runId = sessionRunMap.get(activeSessionId) || currentRunId;
|
|
1864
|
-
const summary = findSummaryForRun(runId) || null;
|
|
1865
|
-
renderRunSummaryMatrix(summary);
|
|
1866
|
-
}
|
|
1867
|
-
|
|
1868
|
-
async function cancelQueueItem(runId) {
|
|
1869
|
-
await fetch(`/api/queue/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
|
|
1870
|
-
await loadAgentState();
|
|
1871
|
-
}
|
|
1872
|
-
|
|
1873
|
-
async function retryRun(runId) {
|
|
1874
|
-
await fetch(`/api/queue/${encodeURIComponent(runId)}/retry`, { method: 'POST' });
|
|
1875
|
-
await loadAgentState();
|
|
1876
|
-
}
|
|
1877
|
-
|
|
1878
|
-
async function reorderQueueItem(runId, direction) {
|
|
1879
|
-
await fetch(`/api/queue/${encodeURIComponent(runId)}/reorder`, {
|
|
1880
|
-
method: 'POST',
|
|
1881
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1882
|
-
body: JSON.stringify({ direction }),
|
|
1883
|
-
});
|
|
1884
|
-
await loadAgentState();
|
|
1885
|
-
}
|
|
1886
|
-
|
|
1887
|
-
function ensureSessionForRun(run, makeActive = false) {
|
|
1888
|
-
if (!run?.runId) return null;
|
|
1889
|
-
let sessionId = runSessionMap.get(run.runId);
|
|
1890
|
-
if (!sessionId) {
|
|
1891
|
-
sessionId = createSession(run.title || `Run ${run.runId}`, run.phase === 'failed' ? 'error' : run.phase === 'completed' ? 'ok' : 'running', run.runId);
|
|
1892
|
-
const meta = [`runId: ${run.runId}`, `phase: ${run.phase}`];
|
|
1893
|
-
if (Array.isArray(run.targetSourcePaths) && run.targetSourcePaths.length > 0) {
|
|
1894
|
-
meta.push(`targets: ${run.targetSourcePaths.length}`);
|
|
1895
|
-
}
|
|
1896
|
-
appendToSession(sessionId, `ℹ ${meta.join(' • ')}`, false, true);
|
|
1897
|
-
}
|
|
1898
|
-
if (makeActive) switchSession(sessionId);
|
|
1899
|
-
return sessionId;
|
|
1900
|
-
}
|
|
1901
|
-
|
|
1902
|
-
function applyAgentStateSnapshot(state) {
|
|
1903
|
-
if (!state) return;
|
|
1904
|
-
agentQueueState = Array.isArray(state.queue) ? state.queue : [];
|
|
1905
|
-
agentRunsState = Array.isArray(state.runs) ? state.runs : [];
|
|
1906
|
-
agentActiveRun = state.activeRun || null;
|
|
1907
|
-
updateQueueBadge(agentQueueState.length);
|
|
1908
|
-
if (agentActiveRun?.runId) {
|
|
1909
|
-
currentRunId = agentActiveRun.runId;
|
|
1910
|
-
ensureSessionForRun(agentActiveRun);
|
|
1911
|
-
setAgentRunning(['starting', 'running', 'validating'].includes(agentActiveRun.phase));
|
|
1912
|
-
} else {
|
|
1913
|
-
setAgentRunning(false);
|
|
1914
|
-
}
|
|
1915
|
-
const latestSummary = [...agentRunsState].reverse().find((r) => Array.isArray(r.fileOutcomes) && r.fileOutcomes.length > 0);
|
|
1916
|
-
if (latestSummary) {
|
|
1917
|
-
lastRunSummary = latestSummary;
|
|
1918
|
-
}
|
|
1919
|
-
syncSummaryForActiveSession();
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
async function loadAgentState() {
|
|
1923
|
-
try {
|
|
1924
|
-
const res = await fetch('/api/agent/state');
|
|
1925
|
-
if (!res.ok) return;
|
|
1926
|
-
const state = await res.json();
|
|
1927
|
-
applyAgentStateSnapshot(state);
|
|
1928
|
-
} catch {}
|
|
1929
|
-
}
|
|
1930
|
-
|
|
1931
|
-
function upsertRunState(run) {
|
|
1932
|
-
if (!run?.runId) return;
|
|
1933
|
-
const idx = agentRunsState.findIndex((r) => r.runId === run.runId);
|
|
1934
|
-
if (idx >= 0) agentRunsState[idx] = { ...agentRunsState[idx], ...run };
|
|
1935
|
-
else agentRunsState.push(run);
|
|
1936
|
-
if (agentRunsState.length > 80) {
|
|
1937
|
-
agentRunsState = agentRunsState.slice(agentRunsState.length - 80);
|
|
1938
|
-
}
|
|
1939
|
-
}
|
|
1940
|
-
|
|
1941
|
-
function updateSessionFromRun(run) {
|
|
1942
|
-
if (!run?.runId) return;
|
|
1943
|
-
upsertRunState(run);
|
|
1944
|
-
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
1945
|
-
setAgentRunning(true);
|
|
1946
|
-
currentRunId = run.runId;
|
|
1947
|
-
const sid = ensureSessionForRun(run);
|
|
1948
|
-
if (sid) {
|
|
1949
|
-
runningSessionId = sid;
|
|
1950
|
-
if (activeSessionId === sid) {
|
|
1951
|
-
document.getElementById('agentPanelTitle').textContent = '🤖 ' + (run.title || 'Agent');
|
|
1952
|
-
document.getElementById('agentPanelStatus').textContent = run.phase === 'validating' ? 'проверяю…' : 'работает…';
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
const sid = runSessionMap.get(run.runId);
|
|
1958
|
-
if (sid) {
|
|
1959
|
-
const status = run.phase === 'completed' ? 'ok' : run.phase === 'canceled' ? 'error' : run.phase === 'failed' ? 'error' : 'info';
|
|
1960
|
-
updateSessionStatus(sid, status);
|
|
1961
|
-
if (runningSessionId === sid) runningSessionId = null;
|
|
1962
|
-
if (activeSessionId === sid) {
|
|
1963
|
-
document.getElementById('agentPanelStatus').textContent =
|
|
1964
|
-
run.phase === 'completed' ? '✅ готово' : run.phase === 'canceled' ? '⏹ отменено' : run.phase === 'failed' ? '❌ ошибка' : run.phase;
|
|
1965
|
-
}
|
|
1966
|
-
}
|
|
1967
|
-
if (run.phase === 'completed' || run.phase === 'failed' || run.phase === 'canceled') {
|
|
1968
|
-
if (agentActiveRun?.runId === run.runId) agentActiveRun = null;
|
|
1969
|
-
if (currentRunId === run.runId) currentRunId = null;
|
|
1970
|
-
setAgentRunning(false);
|
|
1971
|
-
}
|
|
1972
|
-
syncSummaryForActiveSession();
|
|
1973
|
-
}
|
|
1974
|
-
|
|
1975
|
-
function refreshPathActivityFromState() {
|
|
1976
|
-
agentQueuedPaths.clear();
|
|
1977
|
-
(agentQueueState || []).forEach((q) => {
|
|
1978
|
-
getTaskFilePaths(q.task, q.featureKey, q.filePath, q.selectedFilePaths).forEach((p) => agentQueuedPaths.add(p));
|
|
1979
|
-
});
|
|
1980
|
-
if (!agentActiveRun || !['starting', 'running', 'validating'].includes(agentActiveRun.phase)) {
|
|
1981
|
-
agentRunningPaths.clear();
|
|
1982
|
-
}
|
|
1983
|
-
}
|
|
1984
|
-
|
|
1985
|
-
async function cancelAgent() {
|
|
1986
|
-
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1987
|
-
await loadAgentState();
|
|
1988
|
-
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1989
|
-
if (runningSessionId) {
|
|
1990
|
-
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1798
|
+
function updateQueueBadge(n) {
|
|
1799
|
+
document.getElementById('agentQueueCount').textContent = n;
|
|
1800
|
+
document.getElementById('agentQueueBadge').style.display = n > 0 ? 'inline' : 'none';
|
|
1801
|
+
document.getElementById('agentQueueClearBtn').style.display = n > 0 ? 'inline-block' : 'none';
|
|
1802
|
+
renderQueuePanel();
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
function renderQueuePanel() {
|
|
1806
|
+
const panel = document.getElementById('agentQueuePanel');
|
|
1807
|
+
if (!panel) return;
|
|
1808
|
+
if (!Array.isArray(agentQueueState) || agentQueueState.length === 0) {
|
|
1809
|
+
panel.style.display = 'none';
|
|
1810
|
+
panel.innerHTML = '';
|
|
1811
|
+
return;
|
|
1812
|
+
}
|
|
1813
|
+
panel.style.display = 'block';
|
|
1814
|
+
panel.innerHTML = `
|
|
1815
|
+
<div class="agent-queue-title">Очередь задач (${agentQueueState.length})</div>
|
|
1816
|
+
${agentQueueState.map((item, idx) => `
|
|
1817
|
+
<div class="agent-queue-item">
|
|
1818
|
+
<span class="agent-queue-pos">#${item.position ?? idx + 1}</span>
|
|
1819
|
+
<span title="${escapeHtml(item.title || '')}">${escapeHtml(item.title || item.task || item.runId)}</span>
|
|
1820
|
+
<span style="color:var(--dim)">(${escapeHtml(item.task || 'task')})</span>
|
|
1821
|
+
<div class="agent-queue-actions">
|
|
1822
|
+
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','up')" title="Сдвинуть вверх">↑</button>
|
|
1823
|
+
<button class="agent-queue-action" onclick="reorderQueueItem('${item.runId}','down')" title="Сдвинуть вниз">↓</button>
|
|
1824
|
+
<button class="agent-queue-action" onclick="cancelQueueItem('${item.runId}')" title="Отменить задачу">✕</button>
|
|
1825
|
+
<button class="agent-queue-action" onclick="retryRun('${item.runId}')" title="Retry">↻</button>
|
|
1826
|
+
</div>
|
|
1827
|
+
</div>
|
|
1828
|
+
`).join('')}
|
|
1829
|
+
`;
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
function renderRunSummaryMatrix(summary = lastRunSummary) {
|
|
1833
|
+
const box = document.getElementById('agentSummaryMatrix');
|
|
1834
|
+
if (!box) return;
|
|
1835
|
+
const outcomes = summary?.fileOutcomes || [];
|
|
1836
|
+
if (!Array.isArray(outcomes) || outcomes.length === 0) {
|
|
1837
|
+
box.style.display = 'none';
|
|
1838
|
+
box.innerHTML = '';
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
const stats = summary?.validationStats || {};
|
|
1842
|
+
box.style.display = 'block';
|
|
1843
|
+
box.innerHTML = `
|
|
1844
|
+
<div class="agent-summary-title">
|
|
1845
|
+
Матрица итогов (run: ${escapeHtml(summary?.runId || '—')}) • covered: ${stats.covered ?? 0} • not-covered: ${stats.notCovered ?? 0} • blocked: ${stats.blocked ?? 0} • infra: ${stats.infra ?? 0}
|
|
1846
|
+
</div>
|
|
1847
|
+
${outcomes.map((entry) => `
|
|
1848
|
+
<div class="agent-summary-row">
|
|
1849
|
+
<span class="agent-summary-status-${entry.status}">${escapeHtml(entry.status)}</span>
|
|
1850
|
+
<span title="${escapeHtml(entry.sourcePath || '')}">${escapeHtml(entry.sourcePath || '')}</span>
|
|
1851
|
+
<span style="color:var(--dim)">${escapeHtml(entry.testFile || entry.reason || '')}</span>
|
|
1852
|
+
</div>
|
|
1853
|
+
`).join('')}
|
|
1854
|
+
`;
|
|
1855
|
+
}
|
|
1856
|
+
|
|
1857
|
+
function findSummaryForRun(runId) {
|
|
1858
|
+
if (!runId) return null;
|
|
1859
|
+
return (agentRunsState || []).find((r) => r.runId === runId && Array.isArray(r.fileOutcomes) && r.fileOutcomes.length > 0) || null;
|
|
1860
|
+
}
|
|
1861
|
+
|
|
1862
|
+
function syncSummaryForActiveSession() {
|
|
1863
|
+
const runId = sessionRunMap.get(activeSessionId) || currentRunId;
|
|
1864
|
+
const summary = findSummaryForRun(runId) || null;
|
|
1865
|
+
renderRunSummaryMatrix(summary);
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
async function cancelQueueItem(runId) {
|
|
1869
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/cancel`, { method: 'POST' });
|
|
1870
|
+
await loadAgentState();
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
async function retryRun(runId) {
|
|
1874
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/retry`, { method: 'POST' });
|
|
1875
|
+
await loadAgentState();
|
|
1876
|
+
}
|
|
1877
|
+
|
|
1878
|
+
async function reorderQueueItem(runId, direction) {
|
|
1879
|
+
await fetch(`/api/queue/${encodeURIComponent(runId)}/reorder`, {
|
|
1880
|
+
method: 'POST',
|
|
1881
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1882
|
+
body: JSON.stringify({ direction }),
|
|
1883
|
+
});
|
|
1884
|
+
await loadAgentState();
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
function ensureSessionForRun(run, makeActive = false) {
|
|
1888
|
+
if (!run?.runId) return null;
|
|
1889
|
+
let sessionId = runSessionMap.get(run.runId);
|
|
1890
|
+
if (!sessionId) {
|
|
1891
|
+
sessionId = createSession(run.title || `Run ${run.runId}`, run.phase === 'failed' ? 'error' : run.phase === 'completed' ? 'ok' : 'running', run.runId);
|
|
1892
|
+
const meta = [`runId: ${run.runId}`, `phase: ${run.phase}`];
|
|
1893
|
+
if (Array.isArray(run.targetSourcePaths) && run.targetSourcePaths.length > 0) {
|
|
1894
|
+
meta.push(`targets: ${run.targetSourcePaths.length}`);
|
|
1895
|
+
}
|
|
1896
|
+
appendToSession(sessionId, `ℹ ${meta.join(' • ')}`, false, true);
|
|
1897
|
+
}
|
|
1898
|
+
if (makeActive) switchSession(sessionId);
|
|
1899
|
+
return sessionId;
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
function applyAgentStateSnapshot(state) {
|
|
1903
|
+
if (!state) return;
|
|
1904
|
+
agentQueueState = Array.isArray(state.queue) ? state.queue : [];
|
|
1905
|
+
agentRunsState = Array.isArray(state.runs) ? state.runs : [];
|
|
1906
|
+
agentActiveRun = state.activeRun || null;
|
|
1907
|
+
updateQueueBadge(agentQueueState.length);
|
|
1908
|
+
if (agentActiveRun?.runId) {
|
|
1909
|
+
currentRunId = agentActiveRun.runId;
|
|
1910
|
+
ensureSessionForRun(agentActiveRun);
|
|
1911
|
+
setAgentRunning(['starting', 'running', 'validating'].includes(agentActiveRun.phase));
|
|
1912
|
+
} else {
|
|
1913
|
+
setAgentRunning(false);
|
|
1914
|
+
}
|
|
1915
|
+
const latestSummary = [...agentRunsState].reverse().find((r) => Array.isArray(r.fileOutcomes) && r.fileOutcomes.length > 0);
|
|
1916
|
+
if (latestSummary) {
|
|
1917
|
+
lastRunSummary = latestSummary;
|
|
1918
|
+
}
|
|
1919
|
+
syncSummaryForActiveSession();
|
|
1920
|
+
}
|
|
1921
|
+
|
|
1922
|
+
async function loadAgentState() {
|
|
1923
|
+
try {
|
|
1924
|
+
const res = await fetch('/api/agent/state');
|
|
1925
|
+
if (!res.ok) return;
|
|
1926
|
+
const state = await res.json();
|
|
1927
|
+
applyAgentStateSnapshot(state);
|
|
1928
|
+
} catch {}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
function upsertRunState(run) {
|
|
1932
|
+
if (!run?.runId) return;
|
|
1933
|
+
const idx = agentRunsState.findIndex((r) => r.runId === run.runId);
|
|
1934
|
+
if (idx >= 0) agentRunsState[idx] = { ...agentRunsState[idx], ...run };
|
|
1935
|
+
else agentRunsState.push(run);
|
|
1936
|
+
if (agentRunsState.length > 80) {
|
|
1937
|
+
agentRunsState = agentRunsState.slice(agentRunsState.length - 80);
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
function updateSessionFromRun(run) {
|
|
1942
|
+
if (!run?.runId) return;
|
|
1943
|
+
upsertRunState(run);
|
|
1944
|
+
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
1945
|
+
setAgentRunning(true);
|
|
1946
|
+
currentRunId = run.runId;
|
|
1947
|
+
const sid = ensureSessionForRun(run);
|
|
1948
|
+
if (sid) {
|
|
1949
|
+
runningSessionId = sid;
|
|
1950
|
+
if (activeSessionId === sid) {
|
|
1951
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + (run.title || 'Agent');
|
|
1952
|
+
document.getElementById('agentPanelStatus').textContent = run.phase === 'validating' ? 'проверяю…' : 'работает…';
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
return;
|
|
1956
|
+
}
|
|
1957
|
+
const sid = runSessionMap.get(run.runId);
|
|
1958
|
+
if (sid) {
|
|
1959
|
+
const status = run.phase === 'completed' ? 'ok' : run.phase === 'canceled' ? 'error' : run.phase === 'failed' ? 'error' : 'info';
|
|
1960
|
+
updateSessionStatus(sid, status);
|
|
1961
|
+
if (runningSessionId === sid) runningSessionId = null;
|
|
1962
|
+
if (activeSessionId === sid) {
|
|
1963
|
+
document.getElementById('agentPanelStatus').textContent =
|
|
1964
|
+
run.phase === 'completed' ? '✅ готово' : run.phase === 'canceled' ? '⏹ отменено' : run.phase === 'failed' ? '❌ ошибка' : run.phase;
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (run.phase === 'completed' || run.phase === 'failed' || run.phase === 'canceled') {
|
|
1968
|
+
if (agentActiveRun?.runId === run.runId) agentActiveRun = null;
|
|
1969
|
+
if (currentRunId === run.runId) currentRunId = null;
|
|
1970
|
+
setAgentRunning(false);
|
|
1971
|
+
}
|
|
1972
|
+
syncSummaryForActiveSession();
|
|
1973
|
+
}
|
|
1974
|
+
|
|
1975
|
+
function refreshPathActivityFromState() {
|
|
1976
|
+
agentQueuedPaths.clear();
|
|
1977
|
+
(agentQueueState || []).forEach((q) => {
|
|
1978
|
+
getTaskFilePaths(q.task, q.featureKey, q.filePath, q.selectedFilePaths).forEach((p) => agentQueuedPaths.add(p));
|
|
1979
|
+
});
|
|
1980
|
+
if (!agentActiveRun || !['starting', 'running', 'validating'].includes(agentActiveRun.phase)) {
|
|
1981
|
+
agentRunningPaths.clear();
|
|
1982
|
+
}
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
async function cancelAgent() {
|
|
1986
|
+
await fetch('/api/cancel-agent', { method: 'POST' });
|
|
1987
|
+
await loadAgentState();
|
|
1988
|
+
document.getElementById('agentPanelStatus').textContent = '⏹ сброшен';
|
|
1989
|
+
if (runningSessionId) {
|
|
1990
|
+
appendToSession(runningSessionId, '⏹ Состояние агента сброшено (очередь очищена)', false);
|
|
1991
1991
|
updateSessionStatus(runningSessionId, 'error');
|
|
1992
1992
|
runningSessionId = null;
|
|
1993
1993
|
} else {
|
|
@@ -1995,11 +1995,11 @@ async function cancelAgent() {
|
|
|
1995
1995
|
}
|
|
1996
1996
|
}
|
|
1997
1997
|
|
|
1998
|
-
async function clearAgentQueue() {
|
|
1999
|
-
await fetch('/api/clear-queue', { method: 'POST' });
|
|
2000
|
-
await loadAgentState();
|
|
2001
|
-
appendTerminalLine('🗑 Очередь очищена', false);
|
|
2002
|
-
}
|
|
1998
|
+
async function clearAgentQueue() {
|
|
1999
|
+
await fetch('/api/clear-queue', { method: 'POST' });
|
|
2000
|
+
await loadAgentState();
|
|
2001
|
+
appendTerminalLine('🗑 Очередь очищена', false);
|
|
2002
|
+
}
|
|
2003
2003
|
|
|
2004
2004
|
// ─── File row more menu ──────────────────────────────────────────────────────
|
|
2005
2005
|
let _openFileMenu = null;
|
|
@@ -2126,9 +2126,12 @@ async function reauthAgent() {
|
|
|
2126
2126
|
}
|
|
2127
2127
|
const id = createSession('🔑 Перелогинивание');
|
|
2128
2128
|
runningSessionId = id;
|
|
2129
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
2130
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
2129
2131
|
document.getElementById('agentPanelTitle').textContent = '🔑 Перелогинивание';
|
|
2130
2132
|
document.getElementById('agentPanelStatus').textContent = 'выполняю…';
|
|
2131
2133
|
await fetch('/api/agent-reauth', { method: 'POST' });
|
|
2134
|
+
document.getElementById('agentPanelStatus').textContent = 'жду завершения браузера…';
|
|
2132
2135
|
}
|
|
2133
2136
|
|
|
2134
2137
|
async function runAgentTask(task, featureKey, filePath, selectedFilePaths) {
|
|
@@ -2271,14 +2274,14 @@ function renderStats() {
|
|
|
2271
2274
|
const totalBytes = src.reduce((acc, m) => acc + (m.size || 0), 0);
|
|
2272
2275
|
const avgKb = src.length ? Math.round(totalBytes / src.length / 1024) : 0;
|
|
2273
2276
|
const noiseRatio = src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0;
|
|
2274
|
-
const missingStructured = serviceSources.filter(m => !m.hasTests).length;
|
|
2275
|
-
items = [
|
|
2276
|
-
{ v: serviceSources.length, l: 'Источники логов' },
|
|
2277
|
-
{ v: avgKb + ' KB', l: 'Средний объём/файл' },
|
|
2278
|
-
{ v: noiseRatio + '%', l: 'Коэффициент шума' },
|
|
2279
|
-
{ v: errorSignal, l: 'Сигнал ошибок', c: errorSignal ? '#f85149' : undefined },
|
|
2280
|
-
{ v: missingStructured, l: 'Не хватает полей', c: missingStructured ? '#e3b341' : undefined },
|
|
2281
|
-
];
|
|
2277
|
+
const missingStructured = serviceSources.filter(m => !m.hasTests).length;
|
|
2278
|
+
items = [
|
|
2279
|
+
{ v: serviceSources.length, l: 'Источники логов' },
|
|
2280
|
+
{ v: avgKb + ' KB', l: 'Средний объём/файл' },
|
|
2281
|
+
{ v: noiseRatio + '%', l: 'Коэффициент шума' },
|
|
2282
|
+
{ v: errorSignal, l: 'Сигнал ошибок', c: errorSignal ? '#f85149' : undefined },
|
|
2283
|
+
{ v: missingStructured, l: 'Не хватает полей', c: missingStructured ? '#e3b341' : undefined },
|
|
2284
|
+
];
|
|
2282
2285
|
} else if (D.hasConfig && D.features) {
|
|
2283
2286
|
const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
|
|
2284
2287
|
items = [
|
|
@@ -2307,10 +2310,10 @@ function renderStats() {
|
|
|
2307
2310
|
|
|
2308
2311
|
function renderModeSwitch() {
|
|
2309
2312
|
const root = document.getElementById('modeSwitch');
|
|
2310
|
-
const modes = [
|
|
2311
|
-
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2312
|
-
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2313
|
-
];
|
|
2313
|
+
const modes = [
|
|
2314
|
+
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2315
|
+
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2316
|
+
];
|
|
2314
2317
|
root.innerHTML = modes.map(m => `
|
|
2315
2318
|
<button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
|
|
2316
2319
|
<span>${m.label}</span>
|
|
@@ -2328,15 +2331,15 @@ function renderSidebar() {
|
|
|
2328
2331
|
const tabs = document.getElementById('viewTabs');
|
|
2329
2332
|
const extra = document.getElementById('sidebarExtra');
|
|
2330
2333
|
|
|
2331
|
-
if (contextMode === 'observability') {
|
|
2332
|
-
tabs.style.display = 'none';
|
|
2333
|
-
extra.innerHTML = `
|
|
2334
|
-
<div class="sidebar-label">Фокус наблюдаемости</div>
|
|
2335
|
-
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
|
|
2336
|
-
Источники логов и качество сигналов для триажа инцидентов.
|
|
2337
|
-
</div>`;
|
|
2338
|
-
return;
|
|
2339
|
-
}
|
|
2334
|
+
if (contextMode === 'observability') {
|
|
2335
|
+
tabs.style.display = 'none';
|
|
2336
|
+
extra.innerHTML = `
|
|
2337
|
+
<div class="sidebar-label">Фокус наблюдаемости</div>
|
|
2338
|
+
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
|
|
2339
|
+
Источники логов и качество сигналов для триажа инцидентов.
|
|
2340
|
+
</div>`;
|
|
2341
|
+
return;
|
|
2342
|
+
}
|
|
2340
2343
|
|
|
2341
2344
|
tabs.style.display = 'flex';
|
|
2342
2345
|
document.querySelectorAll('.view-tab').forEach(t =>
|
|
@@ -2396,13 +2399,13 @@ function renderQaOnboarding() {
|
|
|
2396
2399
|
return `<div class="onboarding-block"><h3>QA Coverage: что это?</h3><p>Этот экран помогает найти пробелы в тестах: сначала проверь покрытие по фичам, затем открой критичные непокрытые файлы и запусти генерацию/фиксы тестов.</p></div>`;
|
|
2397
2400
|
}
|
|
2398
2401
|
|
|
2399
|
-
function renderObservability(c) {
|
|
2400
|
-
const src = D.modules.filter(m => m.type !== 'test');
|
|
2401
|
-
const sourceGroups = [
|
|
2402
|
-
{ label: 'Сервисы', count: src.filter(m => m.type === 'service').length },
|
|
2403
|
-
{ label: 'Утилиты', count: src.filter(m => m.type === 'util').length },
|
|
2404
|
-
{ label: 'Прочее', count: src.filter(m => m.type === 'other').length },
|
|
2405
|
-
].filter(x => x.count > 0);
|
|
2402
|
+
function renderObservability(c) {
|
|
2403
|
+
const src = D.modules.filter(m => m.type !== 'test');
|
|
2404
|
+
const sourceGroups = [
|
|
2405
|
+
{ label: 'Сервисы', count: src.filter(m => m.type === 'service').length },
|
|
2406
|
+
{ label: 'Утилиты', count: src.filter(m => m.type === 'util').length },
|
|
2407
|
+
{ label: 'Прочее', count: src.filter(m => m.type === 'other').length },
|
|
2408
|
+
].filter(x => x.count > 0);
|
|
2406
2409
|
const errorSignal = Object.values(D.testErrors || {}).reduce((acc, v) => acc + (v?.failed || 0), 0);
|
|
2407
2410
|
const missingStructured = src.filter(m => (m.type === 'service' || m.type === 'util') && !m.hasTests).slice(0, 8);
|
|
2408
2411
|
const noisy = [...src]
|
|
@@ -2410,11 +2413,11 @@ function renderObservability(c) {
|
|
|
2410
2413
|
.slice(0, 5)
|
|
2411
2414
|
.map(m => ({ name: m.name, rel: m.relativePath, kb: Math.round((m.size || 0) / 1024) }));
|
|
2412
2415
|
|
|
2413
|
-
c.innerHTML = `
|
|
2414
|
-
<div class="onboarding-block">
|
|
2415
|
-
<h3>Наблюдаемость: что это?</h3>
|
|
2416
|
-
<p>Экран для контроля отношения сигнал/шум: откуда идут логи, где растёт объём, сколько шума и какие поля/сигналы нужно структурировать в первую очередь.</p>
|
|
2417
|
-
</div>
|
|
2416
|
+
c.innerHTML = `
|
|
2417
|
+
<div class="onboarding-block">
|
|
2418
|
+
<h3>Наблюдаемость: что это?</h3>
|
|
2419
|
+
<p>Экран для контроля отношения сигнал/шум: откуда идут логи, где растёт объём, сколько шума и какие поля/сигналы нужно структурировать в первую очередь.</p>
|
|
2420
|
+
</div>
|
|
2418
2421
|
|
|
2419
2422
|
<div class="obs-grid">
|
|
2420
2423
|
<div class="obs-card">
|
|
@@ -2424,33 +2427,33 @@ function renderObservability(c) {
|
|
|
2424
2427
|
</div>
|
|
2425
2428
|
</div>
|
|
2426
2429
|
|
|
2427
|
-
<div class="obs-card">
|
|
2428
|
-
<div class="obs-title">Объём логов (топ файлов)</div>
|
|
2429
|
-
<div class="obs-list">
|
|
2430
|
-
${noisy.map(n => `<div class="obs-list-item"><span title="${n.rel}">${n.name}</span><strong>${n.kb} KB</strong></div>`).join('') || '<div class="obs-sub">Нет файлов</div>'}
|
|
2431
|
-
</div>
|
|
2432
|
-
</div>
|
|
2433
|
-
|
|
2434
|
-
<div class="obs-card">
|
|
2435
|
-
<div class="obs-title">Коэффициент шума</div>
|
|
2436
|
-
<div class="obs-value">${src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0}%</div>
|
|
2437
|
-
<div class="obs-sub">Доля источников без тестового контроля (прокси-метрика шумного логирования).</div>
|
|
2438
|
-
</div>
|
|
2439
|
-
|
|
2440
|
-
<div class="obs-card">
|
|
2441
|
-
<div class="obs-title">Сигнал ошибок</div>
|
|
2442
|
-
<div class="obs-value" style="color:${errorSignal ? 'var(--red)' : 'var(--green)'}">${errorSignal}</div>
|
|
2443
|
-
<div class="obs-sub">Сумма упавших тестов из последнего прогона как индикатор нестабильности сигналов.</div>
|
|
2444
|
-
</div>
|
|
2445
|
-
|
|
2446
|
-
<div class="obs-card" style="grid-column:1 / -1">
|
|
2447
|
-
<div class="obs-title">Недостающие структурированные поля (кандидаты)</div>
|
|
2448
|
-
<div class="obs-list">
|
|
2449
|
-
${missingStructured.map(m => `<div class="obs-list-item"><span>${m.relativePath}</span><strong>нужна схема</strong></div>`).join('') || '<div class="obs-sub">Явных кандидатов не найдено</div>'}
|
|
2450
|
-
</div>
|
|
2451
|
-
</div>
|
|
2452
|
-
</div>`;
|
|
2453
|
-
}
|
|
2430
|
+
<div class="obs-card">
|
|
2431
|
+
<div class="obs-title">Объём логов (топ файлов)</div>
|
|
2432
|
+
<div class="obs-list">
|
|
2433
|
+
${noisy.map(n => `<div class="obs-list-item"><span title="${n.rel}">${n.name}</span><strong>${n.kb} KB</strong></div>`).join('') || '<div class="obs-sub">Нет файлов</div>'}
|
|
2434
|
+
</div>
|
|
2435
|
+
</div>
|
|
2436
|
+
|
|
2437
|
+
<div class="obs-card">
|
|
2438
|
+
<div class="obs-title">Коэффициент шума</div>
|
|
2439
|
+
<div class="obs-value">${src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0}%</div>
|
|
2440
|
+
<div class="obs-sub">Доля источников без тестового контроля (прокси-метрика шумного логирования).</div>
|
|
2441
|
+
</div>
|
|
2442
|
+
|
|
2443
|
+
<div class="obs-card">
|
|
2444
|
+
<div class="obs-title">Сигнал ошибок</div>
|
|
2445
|
+
<div class="obs-value" style="color:${errorSignal ? 'var(--red)' : 'var(--green)'}">${errorSignal}</div>
|
|
2446
|
+
<div class="obs-sub">Сумма упавших тестов из последнего прогона как индикатор нестабильности сигналов.</div>
|
|
2447
|
+
</div>
|
|
2448
|
+
|
|
2449
|
+
<div class="obs-card" style="grid-column:1 / -1">
|
|
2450
|
+
<div class="obs-title">Недостающие структурированные поля (кандидаты)</div>
|
|
2451
|
+
<div class="obs-list">
|
|
2452
|
+
${missingStructured.map(m => `<div class="obs-list-item"><span>${m.relativePath}</span><strong>нужна схема</strong></div>`).join('') || '<div class="obs-sub">Явных кандидатов не найдено</div>'}
|
|
2453
|
+
</div>
|
|
2454
|
+
</div>
|
|
2455
|
+
</div>`;
|
|
2456
|
+
}
|
|
2454
2457
|
|
|
2455
2458
|
function backToFeatureDetail() {
|
|
2456
2459
|
drillTestType = null;
|
|
@@ -2468,15 +2471,15 @@ function toPct(v) {
|
|
|
2468
2471
|
return Math.round((v || 0) * 100) + '%';
|
|
2469
2472
|
}
|
|
2470
2473
|
|
|
2471
|
-
function renderObservabilityOverview(c) {
|
|
2472
|
-
const o = D.observability;
|
|
2473
|
-
if (!o) return '';
|
|
2474
|
-
const metrics = [
|
|
2475
|
-
['Коэффициент шума', toPct(o.metrics.noise_ratio)],
|
|
2476
|
-
['Действия по ошибкам', toPct(o.metrics.error_actionability)],
|
|
2477
|
-
['Полнота структуры', toPct(o.metrics.structured_completeness)],
|
|
2478
|
-
['Покрытие ключевых сценариев', toPct(o.metrics.coverage_of_key_flows)],
|
|
2479
|
-
];
|
|
2474
|
+
function renderObservabilityOverview(c) {
|
|
2475
|
+
const o = D.observability;
|
|
2476
|
+
if (!o) return '';
|
|
2477
|
+
const metrics = [
|
|
2478
|
+
['Коэффициент шума', toPct(o.metrics.noise_ratio)],
|
|
2479
|
+
['Действия по ошибкам', toPct(o.metrics.error_actionability)],
|
|
2480
|
+
['Полнота структуры', toPct(o.metrics.structured_completeness)],
|
|
2481
|
+
['Покрытие ключевых сценариев', toPct(o.metrics.coverage_of_key_flows)],
|
|
2482
|
+
];
|
|
2480
2483
|
const noisy = (o.topNoisyPatterns || []).slice(0, 5).map(i =>
|
|
2481
2484
|
`<div class="obs-row"><span class="obs-priority-${i.priority}">[${i.priority}]</span> ${escapeHtml(i.pattern)} · x${i.count} → <b>${i.recommendation}</b></div>`
|
|
2482
2485
|
).join('') || '<div class="obs-row">Нет шумных паттернов</div>';
|
|
@@ -2489,27 +2492,27 @@ function renderObservabilityOverview(c) {
|
|
|
2489
2492
|
`<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>`
|
|
2490
2493
|
).join('');
|
|
2491
2494
|
|
|
2492
|
-
return `
|
|
2493
|
-
<div class="observability-panel">
|
|
2494
|
-
<div class="observability-title">Наблюдаемость</div>
|
|
2495
|
-
<div class="obs-metrics">
|
|
2496
|
-
${metrics.map(([l,v]) => `<div class="obs-metric"><div class="obs-metric-v">${v}</div><div class="obs-metric-l">${l}</div></div>`).join('')}
|
|
2497
|
-
</div>
|
|
2498
|
-
<div class="obs-columns">
|
|
2499
|
-
<div class="obs-list">
|
|
2500
|
-
<h4>Топ шумных паттернов</h4>
|
|
2501
|
-
${noisy}
|
|
2502
|
-
</div>
|
|
2503
|
-
<div class="obs-list">
|
|
2504
|
-
<h4>Отсутствующие критичные логи</h4>
|
|
2505
|
-
${missing}
|
|
2506
|
-
</div>
|
|
2507
|
-
</div>
|
|
2508
|
-
<div class="obs-catalog">
|
|
2509
|
-
<h4>Каталог источников логов (топ 10)</h4>
|
|
2510
|
-
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>частота</span><span>владелец</span><span>действие</span></div>
|
|
2511
|
-
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2512
|
-
</div>
|
|
2495
|
+
return `
|
|
2496
|
+
<div class="observability-panel">
|
|
2497
|
+
<div class="observability-title">Наблюдаемость</div>
|
|
2498
|
+
<div class="obs-metrics">
|
|
2499
|
+
${metrics.map(([l,v]) => `<div class="obs-metric"><div class="obs-metric-v">${v}</div><div class="obs-metric-l">${l}</div></div>`).join('')}
|
|
2500
|
+
</div>
|
|
2501
|
+
<div class="obs-columns">
|
|
2502
|
+
<div class="obs-list">
|
|
2503
|
+
<h4>Топ шумных паттернов</h4>
|
|
2504
|
+
${noisy}
|
|
2505
|
+
</div>
|
|
2506
|
+
<div class="obs-list">
|
|
2507
|
+
<h4>Отсутствующие критичные логи</h4>
|
|
2508
|
+
${missing}
|
|
2509
|
+
</div>
|
|
2510
|
+
</div>
|
|
2511
|
+
<div class="obs-catalog">
|
|
2512
|
+
<h4>Каталог источников логов (топ 10)</h4>
|
|
2513
|
+
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>частота</span><span>владелец</span><span>действие</span></div>
|
|
2514
|
+
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2515
|
+
</div>
|
|
2513
2516
|
<div class="obs-row" style="margin-top:8px">
|
|
2514
2517
|
Классификация: мусор ${o.classification.trash} · полезно ${o.classification.useful} · критично ${o.classification.critical}
|
|
2515
2518
|
</div>
|
|
@@ -2547,7 +2550,7 @@ function renderFeatureCards(c) {
|
|
|
2547
2550
|
</div>
|
|
2548
2551
|
</div>` : '';
|
|
2549
2552
|
|
|
2550
|
-
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
2553
|
+
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
2551
2554
|
const grid = document.getElementById('featGrid');
|
|
2552
2555
|
|
|
2553
2556
|
list.forEach(f => {
|
|
@@ -2852,12 +2855,12 @@ function renderFeatureDetail(c) {
|
|
|
2852
2855
|
return;
|
|
2853
2856
|
}
|
|
2854
2857
|
|
|
2855
|
-
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
2856
|
-
const src = mods.filter(m => m.type !== 'test');
|
|
2857
|
-
const untestedSrc = src.filter(m => !m.hasTests);
|
|
2858
|
-
const tst = mods.filter(m => m.type === 'test');
|
|
2859
|
-
const testedCount = src.filter(m => m.hasTests).length;
|
|
2860
|
-
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
2858
|
+
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
2859
|
+
const src = mods.filter(m => m.type !== 'test');
|
|
2860
|
+
const untestedSrc = src.filter(m => !m.hasTests);
|
|
2861
|
+
const tst = mods.filter(m => m.type === 'test');
|
|
2862
|
+
const testedCount = src.filter(m => m.hasTests).length;
|
|
2863
|
+
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
2861
2864
|
|
|
2862
2865
|
const unitCount = feat.unitTestCount ?? tst.filter(m => m.testType === 'unit').length;
|
|
2863
2866
|
const integrationCount = feat.integrationTestCount ?? tst.filter(m => m.testType === 'integration').length;
|
|
@@ -2869,32 +2872,32 @@ function renderFeatureDetail(c) {
|
|
|
2869
2872
|
const integrationFailed = tst.filter(m => m.testType === 'integration' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
2870
2873
|
const e2eFailed = tst.filter(m => m.testType === 'e2e' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
2871
2874
|
|
|
2872
|
-
// Determine what list to show based on active tab
|
|
2873
|
-
// null or 'source' → source files; test type → test files of that type
|
|
2874
|
-
const activeTab = drillTestType || 'source';
|
|
2875
|
-
const sourceList = showOnlyUntestedInFeature ? untestedSrc : src;
|
|
2876
|
-
const listFiles = activeTab === 'source' ? sourceList : tst.filter(m => m.testType === activeTab);
|
|
2877
|
-
const isTestList = activeTab !== 'source';
|
|
2878
|
-
const showUntestedToggle = activeTab === 'source';
|
|
2879
|
-
const meta = TEST_TYPE_META[activeTab];
|
|
2880
|
-
const listLabel = meta
|
|
2881
|
-
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
2882
|
-
: `📁 Файлы фичи (${listFiles.length})`;
|
|
2883
|
-
|
|
2884
|
-
const q = searchQuery.toLowerCase();
|
|
2885
|
-
const filtered = q ? listFiles.filter(m =>
|
|
2886
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2887
|
-
) : listFiles;
|
|
2888
|
-
const rowsRenderKey = `feature:${drillFeatureKey}:${activeTab}:${showOnlyUntestedInFeature ? 1 : 0}:${q}`;
|
|
2889
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2890
|
-
const selectableSource = !isTestList && !!D.agent;
|
|
2891
|
-
const visibleSourcePaths = selectableSource
|
|
2892
|
-
? visibleRows.map(m => m.relativePath.replace(/\\/g, '/'))
|
|
2893
|
-
: [];
|
|
2894
|
-
const selectedVisible = selectableSource
|
|
2895
|
-
? selectedFilesForFeature(drillFeatureKey, visibleRows.map(m => m.relativePath))
|
|
2896
|
-
: [];
|
|
2897
|
-
const selectedCount = selectedVisible.length;
|
|
2875
|
+
// Determine what list to show based on active tab
|
|
2876
|
+
// null or 'source' → source files; test type → test files of that type
|
|
2877
|
+
const activeTab = drillTestType || 'source';
|
|
2878
|
+
const sourceList = showOnlyUntestedInFeature ? untestedSrc : src;
|
|
2879
|
+
const listFiles = activeTab === 'source' ? sourceList : tst.filter(m => m.testType === activeTab);
|
|
2880
|
+
const isTestList = activeTab !== 'source';
|
|
2881
|
+
const showUntestedToggle = activeTab === 'source';
|
|
2882
|
+
const meta = TEST_TYPE_META[activeTab];
|
|
2883
|
+
const listLabel = meta
|
|
2884
|
+
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
2885
|
+
: `📁 Файлы фичи (${listFiles.length})`;
|
|
2886
|
+
|
|
2887
|
+
const q = searchQuery.toLowerCase();
|
|
2888
|
+
const filtered = q ? listFiles.filter(m =>
|
|
2889
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
2890
|
+
) : listFiles;
|
|
2891
|
+
const rowsRenderKey = `feature:${drillFeatureKey}:${activeTab}:${showOnlyUntestedInFeature ? 1 : 0}:${q}`;
|
|
2892
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
2893
|
+
const selectableSource = !isTestList && !!D.agent;
|
|
2894
|
+
const visibleSourcePaths = selectableSource
|
|
2895
|
+
? visibleRows.map(m => m.relativePath.replace(/\\/g, '/'))
|
|
2896
|
+
: [];
|
|
2897
|
+
const selectedVisible = selectableSource
|
|
2898
|
+
? selectedFilesForFeature(drillFeatureKey, visibleRows.map(m => m.relativePath))
|
|
2899
|
+
: [];
|
|
2900
|
+
const selectedCount = selectedVisible.length;
|
|
2898
2901
|
|
|
2899
2902
|
c.innerHTML = `
|
|
2900
2903
|
<div class="drill-header">
|
|
@@ -2920,51 +2923,51 @@ function renderFeatureDetail(c) {
|
|
|
2920
2923
|
${testTypeCard('integration', 'Integration', '🔗', '#58a6ff', integrationCount, activeTab === 'integration', drillFeatureKey, integrationFailed)}
|
|
2921
2924
|
${testTypeCard('e2e', 'E2E', '🎭', '#d2a8ff', e2eCount, activeTab === 'e2e', drillFeatureKey, e2eFailed)}
|
|
2922
2925
|
</div>
|
|
2923
|
-
|
|
2924
|
-
<div class="drill-section-label">${listLabel}</div>
|
|
2925
|
-
${showUntestedToggle ? `
|
|
2926
|
-
<div class="feature-file-filters">
|
|
2927
|
-
<label class="feature-filter-toggle">
|
|
2928
|
-
<input type="checkbox" id="untestedOnlyToggle" ${showOnlyUntestedInFeature ? 'checked' : ''}>
|
|
2929
|
-
<span>Только без тестов</span>
|
|
2930
|
-
<span class="feature-filter-meta">(${untestedSrc.length})</span>
|
|
2931
|
-
</label>
|
|
2932
|
-
</div>` : ''}
|
|
2933
|
-
${selectableSource ? `
|
|
2934
|
-
<div class="bulk-actions">
|
|
2935
|
-
<span class="bulk-actions-count">Выбрано файлов: <b>${selectedCount}</b></span>
|
|
2936
|
-
<button class="bulk-actions-btn" id="bulkSelectAllBtn" ${visibleSourcePaths.length === 0 ? 'disabled' : ''}>☑ Выбрать все (${visibleSourcePaths.length})</button>
|
|
2926
|
+
|
|
2927
|
+
<div class="drill-section-label">${listLabel}</div>
|
|
2928
|
+
${showUntestedToggle ? `
|
|
2929
|
+
<div class="feature-file-filters">
|
|
2930
|
+
<label class="feature-filter-toggle">
|
|
2931
|
+
<input type="checkbox" id="untestedOnlyToggle" ${showOnlyUntestedInFeature ? 'checked' : ''}>
|
|
2932
|
+
<span>Только без тестов</span>
|
|
2933
|
+
<span class="feature-filter-meta">(${untestedSrc.length})</span>
|
|
2934
|
+
</label>
|
|
2935
|
+
</div>` : ''}
|
|
2936
|
+
${selectableSource ? `
|
|
2937
|
+
<div class="bulk-actions">
|
|
2938
|
+
<span class="bulk-actions-count">Выбрано файлов: <b>${selectedCount}</b></span>
|
|
2939
|
+
<button class="bulk-actions-btn" id="bulkSelectAllBtn" ${visibleSourcePaths.length === 0 ? 'disabled' : ''}>☑ Выбрать все (${visibleSourcePaths.length})</button>
|
|
2937
2940
|
<button class="bulk-actions-btn" id="bulkClearBtn" ${selectedCount === 0 ? 'disabled' : ''}>Снять выбор</button>
|
|
2938
2941
|
<button class="bulk-actions-btn primary" id="bulkWriteTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>✍ Написать тесты для выбранных</button>
|
|
2939
2942
|
<button class="bulk-actions-btn" id="bulkRefreshTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>↻ Актуализировать тесты</button>
|
|
2940
2943
|
</div>` : ''}
|
|
2941
|
-
<div class="file-rows" id="fileRows">
|
|
2942
|
-
${filtered.length === 0
|
|
2943
|
-
? `<div style="padding:20px;text-align:center;border:1px dashed var(--border);border-radius:8px;color:var(--dim);font-size:13px">
|
|
2944
|
-
${isTestList
|
|
2945
|
-
? 'Нет тестов этого типа для данной фичи'
|
|
2946
|
-
: (showOnlyUntestedInFeature
|
|
2947
|
-
? 'Все файлы этой фичи уже покрыты тестами'
|
|
2948
|
-
: 'Нет файлов — возможно паттерны в конфиге не совпадают')}
|
|
2949
|
-
</div>`
|
|
2950
|
-
: visibleRows.map(m => fileRow(m, isTestList, drillFeatureKey, selectableSource)).join('')
|
|
2951
|
-
}
|
|
2952
|
-
</div>
|
|
2953
|
-
${hasMoreRows ? `
|
|
2954
|
-
<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)">
|
|
2955
|
-
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
2956
|
-
<button class="bulk-actions-btn" id="fileRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
2957
|
-
</div>` : ''}`;
|
|
2958
|
-
|
|
2959
|
-
const untestedOnlyToggle = document.getElementById('untestedOnlyToggle');
|
|
2960
|
-
if (untestedOnlyToggle) {
|
|
2961
|
-
untestedOnlyToggle.onchange = (e) => {
|
|
2962
|
-
showOnlyUntestedInFeature = !!e.target.checked;
|
|
2963
|
-
renderContent();
|
|
2964
|
-
};
|
|
2965
|
-
}
|
|
2966
|
-
|
|
2967
|
-
if (selectableSource) {
|
|
2944
|
+
<div class="file-rows" id="fileRows">
|
|
2945
|
+
${filtered.length === 0
|
|
2946
|
+
? `<div style="padding:20px;text-align:center;border:1px dashed var(--border);border-radius:8px;color:var(--dim);font-size:13px">
|
|
2947
|
+
${isTestList
|
|
2948
|
+
? 'Нет тестов этого типа для данной фичи'
|
|
2949
|
+
: (showOnlyUntestedInFeature
|
|
2950
|
+
? 'Все файлы этой фичи уже покрыты тестами'
|
|
2951
|
+
: 'Нет файлов — возможно паттерны в конфиге не совпадают')}
|
|
2952
|
+
</div>`
|
|
2953
|
+
: visibleRows.map(m => fileRow(m, isTestList, drillFeatureKey, selectableSource)).join('')
|
|
2954
|
+
}
|
|
2955
|
+
</div>
|
|
2956
|
+
${hasMoreRows ? `
|
|
2957
|
+
<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)">
|
|
2958
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
2959
|
+
<button class="bulk-actions-btn" id="fileRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
2960
|
+
</div>` : ''}`;
|
|
2961
|
+
|
|
2962
|
+
const untestedOnlyToggle = document.getElementById('untestedOnlyToggle');
|
|
2963
|
+
if (untestedOnlyToggle) {
|
|
2964
|
+
untestedOnlyToggle.onchange = (e) => {
|
|
2965
|
+
showOnlyUntestedInFeature = !!e.target.checked;
|
|
2966
|
+
renderContent();
|
|
2967
|
+
};
|
|
2968
|
+
}
|
|
2969
|
+
|
|
2970
|
+
if (selectableSource) {
|
|
2968
2971
|
const allVisibleSelected = visibleSourcePaths.length > 0 && visibleSourcePaths.every((p) => selectedSourceFiles.has(p));
|
|
2969
2972
|
const bulkSelectAllBtn = document.getElementById('bulkSelectAllBtn');
|
|
2970
2973
|
if (bulkSelectAllBtn) {
|
|
@@ -2999,17 +3002,17 @@ function renderFeatureDetail(c) {
|
|
|
2999
3002
|
renderContent();
|
|
3000
3003
|
};
|
|
3001
3004
|
}
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
const fileRowsLoadMoreBtn = document.getElementById('fileRowsLoadMoreBtn');
|
|
3005
|
-
if (fileRowsLoadMoreBtn) {
|
|
3006
|
-
fileRowsLoadMoreBtn.onclick = () => {
|
|
3007
|
-
increaseFileRowsLimit();
|
|
3008
|
-
renderContent();
|
|
3009
|
-
};
|
|
3010
|
-
}
|
|
3011
|
-
|
|
3012
|
-
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
3005
|
+
}
|
|
3006
|
+
|
|
3007
|
+
const fileRowsLoadMoreBtn = document.getElementById('fileRowsLoadMoreBtn');
|
|
3008
|
+
if (fileRowsLoadMoreBtn) {
|
|
3009
|
+
fileRowsLoadMoreBtn.onclick = () => {
|
|
3010
|
+
increaseFileRowsLimit();
|
|
3011
|
+
renderContent();
|
|
3012
|
+
};
|
|
3013
|
+
}
|
|
3014
|
+
|
|
3015
|
+
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
3013
3016
|
card.onclick = async () => {
|
|
3014
3017
|
const type = card.dataset.testtype;
|
|
3015
3018
|
drillTestType = (type === 'source') ? null : type; // 'source' tab = null state
|
|
@@ -3018,11 +3021,11 @@ function renderFeatureDetail(c) {
|
|
|
3018
3021
|
if (type === 'e2e') {
|
|
3019
3022
|
await loadE2ePlan(drillFeatureKey);
|
|
3020
3023
|
}
|
|
3021
|
-
renderContent();
|
|
3022
|
-
};
|
|
3023
|
-
});
|
|
3024
|
-
bindFileRowsClick(c);
|
|
3025
|
-
}
|
|
3024
|
+
renderContent();
|
|
3025
|
+
};
|
|
3026
|
+
});
|
|
3027
|
+
bindFileRowsClick(c);
|
|
3028
|
+
}
|
|
3026
3029
|
|
|
3027
3030
|
const TEST_TYPE_META = {
|
|
3028
3031
|
unit: { label: 'Unit', icon: '🧪', color: '#e3b341', desc: 'Изолированные тесты функций и модулей' },
|
|
@@ -3041,12 +3044,12 @@ function renderTestTypeDetail(c) {
|
|
|
3041
3044
|
m.featureKeys && m.featureKeys.includes(drillFeatureKey)
|
|
3042
3045
|
);
|
|
3043
3046
|
|
|
3044
|
-
const q = searchQuery.toLowerCase();
|
|
3045
|
-
const filtered = q ? tests.filter(m =>
|
|
3046
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3047
|
-
) : tests;
|
|
3048
|
-
const rowsRenderKey = `tests:${drillFeatureKey}:${drillTestType}:${q}`;
|
|
3049
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3047
|
+
const q = searchQuery.toLowerCase();
|
|
3048
|
+
const filtered = q ? tests.filter(m =>
|
|
3049
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3050
|
+
) : tests;
|
|
3051
|
+
const rowsRenderKey = `tests:${drillFeatureKey}:${drillTestType}:${q}`;
|
|
3052
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3050
3053
|
|
|
3051
3054
|
c.innerHTML = `
|
|
3052
3055
|
<div class="drill-header">
|
|
@@ -3061,32 +3064,32 @@ function renderTestTypeDetail(c) {
|
|
|
3061
3064
|
</div>
|
|
3062
3065
|
<div style="font-size:12px;color:var(--dim);margin-bottom:16px">${meta.desc}</div>
|
|
3063
3066
|
|
|
3064
|
-
<div class="file-rows" id="fileRows">
|
|
3065
|
-
${filtered.length === 0
|
|
3066
|
-
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
3067
|
-
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
3068
|
-
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
3069
|
-
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
3070
|
-
</div>`
|
|
3071
|
-
: visibleRows.map(m => fileRow(m, true)).join('')
|
|
3072
|
-
}
|
|
3073
|
-
</div>
|
|
3074
|
-
${hasMoreRows ? `
|
|
3075
|
-
<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)">
|
|
3076
|
-
<span>Показано ${visibleRows.length} из ${filtered.length} тест-файлов</span>
|
|
3077
|
-
<button class="bulk-actions-btn" id="testRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3078
|
-
</div>` : ''}`;
|
|
3079
|
-
|
|
3080
|
-
const testRowsLoadMoreBtn = document.getElementById('testRowsLoadMoreBtn');
|
|
3081
|
-
if (testRowsLoadMoreBtn) {
|
|
3082
|
-
testRowsLoadMoreBtn.onclick = () => {
|
|
3083
|
-
increaseFileRowsLimit();
|
|
3084
|
-
renderContent();
|
|
3085
|
-
};
|
|
3086
|
-
}
|
|
3087
|
-
|
|
3088
|
-
bindFileRowsClick(c);
|
|
3089
|
-
}
|
|
3067
|
+
<div class="file-rows" id="fileRows">
|
|
3068
|
+
${filtered.length === 0
|
|
3069
|
+
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
3070
|
+
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
3071
|
+
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
3072
|
+
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
3073
|
+
</div>`
|
|
3074
|
+
: visibleRows.map(m => fileRow(m, true)).join('')
|
|
3075
|
+
}
|
|
3076
|
+
</div>
|
|
3077
|
+
${hasMoreRows ? `
|
|
3078
|
+
<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)">
|
|
3079
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} тест-файлов</span>
|
|
3080
|
+
<button class="bulk-actions-btn" id="testRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3081
|
+
</div>` : ''}`;
|
|
3082
|
+
|
|
3083
|
+
const testRowsLoadMoreBtn = document.getElementById('testRowsLoadMoreBtn');
|
|
3084
|
+
if (testRowsLoadMoreBtn) {
|
|
3085
|
+
testRowsLoadMoreBtn.onclick = () => {
|
|
3086
|
+
increaseFileRowsLimit();
|
|
3087
|
+
renderContent();
|
|
3088
|
+
};
|
|
3089
|
+
}
|
|
3090
|
+
|
|
3091
|
+
bindFileRowsClick(c);
|
|
3092
|
+
}
|
|
3090
3093
|
|
|
3091
3094
|
function renderUnmappedDetail(c) {
|
|
3092
3095
|
const infraSrc = D.modules.filter(m => m.type !== 'test' && m.isInfra);
|
|
@@ -3094,12 +3097,12 @@ function renderUnmappedDetail(c) {
|
|
|
3094
3097
|
m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)
|
|
3095
3098
|
);
|
|
3096
3099
|
|
|
3097
|
-
const q = searchQuery.toLowerCase();
|
|
3098
|
-
const filtered = q ? unmappedSrc.filter(m =>
|
|
3099
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3100
|
-
) : unmappedSrc;
|
|
3101
|
-
const rowsRenderKey = `unmapped:${q}`;
|
|
3102
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3100
|
+
const q = searchQuery.toLowerCase();
|
|
3101
|
+
const filtered = q ? unmappedSrc.filter(m =>
|
|
3102
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
3103
|
+
) : unmappedSrc;
|
|
3104
|
+
const rowsRenderKey = `unmapped:${q}`;
|
|
3105
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(filtered, rowsRenderKey);
|
|
3103
3106
|
|
|
3104
3107
|
// Build prompt text
|
|
3105
3108
|
const featureList = (D.features || []).map(f => ` • ${f.key} — ${f.label}`).join('\n');
|
|
@@ -3140,44 +3143,44 @@ function renderUnmappedDetail(c) {
|
|
|
3140
3143
|
border-radius:6px; color:var(--blue); font-size:12px; cursor:pointer;
|
|
3141
3144
|
">📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)</button>
|
|
3142
3145
|
</div>
|
|
3143
|
-
<div class="file-rows" id="fileRows">
|
|
3144
|
-
${filtered.length === 0
|
|
3145
|
-
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
3146
|
-
: visibleRows.map(m => fileRow(m)).join('')
|
|
3147
|
-
}
|
|
3148
|
-
</div>
|
|
3149
|
-
${hasMoreRows ? `
|
|
3150
|
-
<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)">
|
|
3151
|
-
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
3152
|
-
<button class="bulk-actions-btn" id="unmappedRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3153
|
-
</div>` : ''}`;
|
|
3146
|
+
<div class="file-rows" id="fileRows">
|
|
3147
|
+
${filtered.length === 0
|
|
3148
|
+
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
3149
|
+
: visibleRows.map(m => fileRow(m)).join('')
|
|
3150
|
+
}
|
|
3151
|
+
</div>
|
|
3152
|
+
${hasMoreRows ? `
|
|
3153
|
+
<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)">
|
|
3154
|
+
<span>Показано ${visibleRows.length} из ${filtered.length} файлов</span>
|
|
3155
|
+
<button class="bulk-actions-btn" id="unmappedRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3156
|
+
</div>` : ''}`;
|
|
3154
3157
|
|
|
3155
3158
|
const runAgentUnmappedBtn = document.getElementById('runAgentUnmapped');
|
|
3156
3159
|
if (runAgentUnmappedBtn) {
|
|
3157
3160
|
runAgentUnmappedBtn.onclick = () => runAgentTask('map-unmapped');
|
|
3158
3161
|
}
|
|
3159
3162
|
|
|
3160
|
-
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
3163
|
+
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
3161
3164
|
navigator.clipboard.writeText(promptText).then(() => {
|
|
3162
3165
|
this.textContent = '✅ Скопировано!';
|
|
3163
3166
|
this.style.color = 'var(--green)';
|
|
3164
3167
|
setTimeout(() => {
|
|
3165
3168
|
this.textContent = `📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)`;
|
|
3166
3169
|
this.style.color = 'var(--blue)';
|
|
3167
|
-
}, 3000);
|
|
3168
|
-
});
|
|
3169
|
-
};
|
|
3170
|
-
|
|
3171
|
-
const unmappedRowsLoadMoreBtn = document.getElementById('unmappedRowsLoadMoreBtn');
|
|
3172
|
-
if (unmappedRowsLoadMoreBtn) {
|
|
3173
|
-
unmappedRowsLoadMoreBtn.onclick = () => {
|
|
3174
|
-
increaseFileRowsLimit();
|
|
3175
|
-
renderContent();
|
|
3176
|
-
};
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
|
-
bindFileRowsClick(c);
|
|
3180
|
-
}
|
|
3170
|
+
}, 3000);
|
|
3171
|
+
});
|
|
3172
|
+
};
|
|
3173
|
+
|
|
3174
|
+
const unmappedRowsLoadMoreBtn = document.getElementById('unmappedRowsLoadMoreBtn');
|
|
3175
|
+
if (unmappedRowsLoadMoreBtn) {
|
|
3176
|
+
unmappedRowsLoadMoreBtn.onclick = () => {
|
|
3177
|
+
increaseFileRowsLimit();
|
|
3178
|
+
renderContent();
|
|
3179
|
+
};
|
|
3180
|
+
}
|
|
3181
|
+
|
|
3182
|
+
bindFileRowsClick(c);
|
|
3183
|
+
}
|
|
3181
3184
|
|
|
3182
3185
|
function fileRow(m, isTest = false, featureKey = null, selectable = false) {
|
|
3183
3186
|
const relPath = m.relativePath.replace(/\\/g, '/');
|
|
@@ -3267,33 +3270,33 @@ function toggleSourceSelection(relPath, checked) {
|
|
|
3267
3270
|
renderContent();
|
|
3268
3271
|
}
|
|
3269
3272
|
|
|
3270
|
-
function renderModuleGrid(c) {
|
|
3271
|
-
const q = searchQuery.toLowerCase();
|
|
3272
|
-
const list = D.modules.filter(m => {
|
|
3273
|
-
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
3274
|
-
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
3275
|
-
return true;
|
|
3276
|
-
});
|
|
3277
|
-
const typeKey = [...activeTypes].sort().join(',');
|
|
3278
|
-
const rowsRenderKey = `modules:${contextMode}:${view}:${typeKey}:${q}`;
|
|
3279
|
-
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(list, rowsRenderKey);
|
|
3280
|
-
|
|
3281
|
-
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
3282
|
-
|
|
3283
|
-
c.innerHTML = `
|
|
3284
|
-
<div class="module-grid" id="modGrid"></div>
|
|
3285
|
-
${hasMoreRows ? `
|
|
3286
|
-
<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)">
|
|
3287
|
-
<span>Показано ${visibleRows.length} из ${list.length} модулей</span>
|
|
3288
|
-
<button class="bulk-actions-btn" id="moduleRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3289
|
-
</div>` : ''}
|
|
3290
|
-
`;
|
|
3291
|
-
const grid = document.getElementById('modGrid');
|
|
3292
|
-
|
|
3293
|
-
visibleRows.forEach(m => {
|
|
3294
|
-
const cov = m.coverage?.lines;
|
|
3295
|
-
const isActive = activePanelKey === m.id;
|
|
3296
|
-
const card = document.createElement('div');
|
|
3273
|
+
function renderModuleGrid(c) {
|
|
3274
|
+
const q = searchQuery.toLowerCase();
|
|
3275
|
+
const list = D.modules.filter(m => {
|
|
3276
|
+
if (activeTypes.size > 0 && !activeTypes.has(m.type)) return false;
|
|
3277
|
+
if (q && !m.name.toLowerCase().includes(q) && !m.relativePath.toLowerCase().includes(q)) return false;
|
|
3278
|
+
return true;
|
|
3279
|
+
});
|
|
3280
|
+
const typeKey = [...activeTypes].sort().join(',');
|
|
3281
|
+
const rowsRenderKey = `modules:${contextMode}:${view}:${typeKey}:${q}`;
|
|
3282
|
+
const { visibleRows, hiddenRows, hasMoreRows } = getFileRowsWindow(list, rowsRenderKey);
|
|
3283
|
+
|
|
3284
|
+
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
3285
|
+
|
|
3286
|
+
c.innerHTML = `
|
|
3287
|
+
<div class="module-grid" id="modGrid"></div>
|
|
3288
|
+
${hasMoreRows ? `
|
|
3289
|
+
<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)">
|
|
3290
|
+
<span>Показано ${visibleRows.length} из ${list.length} модулей</span>
|
|
3291
|
+
<button class="bulk-actions-btn" id="moduleRowsLoadMoreBtn">Показать ещё ${Math.min(FILE_ROWS_LIMIT_STEP, hiddenRows)}</button>
|
|
3292
|
+
</div>` : ''}
|
|
3293
|
+
`;
|
|
3294
|
+
const grid = document.getElementById('modGrid');
|
|
3295
|
+
|
|
3296
|
+
visibleRows.forEach(m => {
|
|
3297
|
+
const cov = m.coverage?.lines;
|
|
3298
|
+
const isActive = activePanelKey === m.id;
|
|
3299
|
+
const card = document.createElement('div');
|
|
3297
3300
|
card.className = 'module-card' + (isActive ? ' active' : '');
|
|
3298
3301
|
card.innerHTML = `
|
|
3299
3302
|
<div class="module-name">${m.name}</div>
|
|
@@ -3305,18 +3308,18 @@ function renderModuleGrid(c) {
|
|
|
3305
3308
|
<span class="badge ${m.hasTests ? 'badge-green' : 'badge-red'}">${m.hasTests ? '✓' : '✗'}</span>
|
|
3306
3309
|
</div>
|
|
3307
3310
|
${cov != null ? `<div class="cov-bar"><div class="cov-fill" style="width:${cov}%;background:${covColor(cov)}"></div></div>` : ''}`;
|
|
3308
|
-
card.onclick = () => openModulePanel(m);
|
|
3309
|
-
grid.appendChild(card);
|
|
3310
|
-
});
|
|
3311
|
-
|
|
3312
|
-
const moduleRowsLoadMoreBtn = document.getElementById('moduleRowsLoadMoreBtn');
|
|
3313
|
-
if (moduleRowsLoadMoreBtn) {
|
|
3314
|
-
moduleRowsLoadMoreBtn.onclick = () => {
|
|
3315
|
-
increaseFileRowsLimit();
|
|
3316
|
-
renderContent();
|
|
3317
|
-
};
|
|
3318
|
-
}
|
|
3319
|
-
}
|
|
3311
|
+
card.onclick = () => openModulePanel(m);
|
|
3312
|
+
grid.appendChild(card);
|
|
3313
|
+
});
|
|
3314
|
+
|
|
3315
|
+
const moduleRowsLoadMoreBtn = document.getElementById('moduleRowsLoadMoreBtn');
|
|
3316
|
+
if (moduleRowsLoadMoreBtn) {
|
|
3317
|
+
moduleRowsLoadMoreBtn.onclick = () => {
|
|
3318
|
+
increaseFileRowsLimit();
|
|
3319
|
+
renderContent();
|
|
3320
|
+
};
|
|
3321
|
+
}
|
|
3322
|
+
}
|
|
3320
3323
|
|
|
3321
3324
|
// ─── Panels ───────────────────────────────────────────────────────────────────
|
|
3322
3325
|
function openFeaturePanel(key) {
|
|
@@ -3527,29 +3530,29 @@ function openUnmappedPanel(files, infraFiles) {
|
|
|
3527
3530
|
document.getElementById('panel').classList.add('open');
|
|
3528
3531
|
}
|
|
3529
3532
|
|
|
3530
|
-
function closePanel() {
|
|
3531
|
-
activePanelKey = null;
|
|
3532
|
-
document.getElementById('panel').classList.remove('open');
|
|
3533
|
-
renderContent();
|
|
3534
|
-
}
|
|
3535
|
-
|
|
3536
|
-
function applyTerminalFiltersFromUi() {
|
|
3537
|
-
terminalSearchQuery = document.getElementById('agentSearchInput')?.value || '';
|
|
3538
|
-
terminalSearchErrorsOnly = !!document.getElementById('agentSearchErrorsOnly')?.checked;
|
|
3539
|
-
terminalSearchCurrentRunOnly = !!document.getElementById('agentSearchCurrentRunOnly')?.checked;
|
|
3540
|
-
terminalSearchRegex = !!document.getElementById('agentSearchRegex')?.checked;
|
|
3541
|
-
renderActiveSession();
|
|
3542
|
-
}
|
|
3543
|
-
|
|
3544
|
-
['agentSearchInput', 'agentSearchErrorsOnly', 'agentSearchCurrentRunOnly', 'agentSearchRegex'].forEach((id) => {
|
|
3545
|
-
const el = document.getElementById(id);
|
|
3546
|
-
if (!el) return;
|
|
3547
|
-
const eventName = id === 'agentSearchInput' ? 'input' : 'change';
|
|
3548
|
-
el.addEventListener(eventName, applyTerminalFiltersFromUi);
|
|
3549
|
-
});
|
|
3550
|
-
|
|
3551
|
-
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
3552
|
-
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
3533
|
+
function closePanel() {
|
|
3534
|
+
activePanelKey = null;
|
|
3535
|
+
document.getElementById('panel').classList.remove('open');
|
|
3536
|
+
renderContent();
|
|
3537
|
+
}
|
|
3538
|
+
|
|
3539
|
+
function applyTerminalFiltersFromUi() {
|
|
3540
|
+
terminalSearchQuery = document.getElementById('agentSearchInput')?.value || '';
|
|
3541
|
+
terminalSearchErrorsOnly = !!document.getElementById('agentSearchErrorsOnly')?.checked;
|
|
3542
|
+
terminalSearchCurrentRunOnly = !!document.getElementById('agentSearchCurrentRunOnly')?.checked;
|
|
3543
|
+
terminalSearchRegex = !!document.getElementById('agentSearchRegex')?.checked;
|
|
3544
|
+
renderActiveSession();
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
['agentSearchInput', 'agentSearchErrorsOnly', 'agentSearchCurrentRunOnly', 'agentSearchRegex'].forEach((id) => {
|
|
3548
|
+
const el = document.getElementById(id);
|
|
3549
|
+
if (!el) return;
|
|
3550
|
+
const eventName = id === 'agentSearchInput' ? 'input' : 'change';
|
|
3551
|
+
el.addEventListener(eventName, applyTerminalFiltersFromUi);
|
|
3552
|
+
});
|
|
3553
|
+
|
|
3554
|
+
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
3555
|
+
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
3553
3556
|
tab.onclick = () => {
|
|
3554
3557
|
if (contextMode !== 'qa') return;
|
|
3555
3558
|
if (tab.classList.contains('disabled')) return;
|
|
@@ -3599,204 +3602,204 @@ window.addEventListener('popstate', () => {
|
|
|
3599
3602
|
});
|
|
3600
3603
|
|
|
3601
3604
|
// ─── Live reload ──────────────────────────────────────────────────────────────
|
|
3602
|
-
function setLiveDot(color, title) {
|
|
3603
|
-
const dot = document.getElementById('liveDot');
|
|
3604
|
-
dot.style.background = color;
|
|
3605
|
-
dot.title = title;
|
|
3606
|
-
}
|
|
3607
|
-
|
|
3608
|
-
function scheduleRefreshData(delayMs = 120) {
|
|
3609
|
-
if (refreshDataTimer) return;
|
|
3610
|
-
refreshDataTimer = setTimeout(() => {
|
|
3611
|
-
refreshDataTimer = null;
|
|
3612
|
-
void refreshData();
|
|
3613
|
-
}, delayMs);
|
|
3614
|
-
}
|
|
3615
|
-
|
|
3616
|
-
async function refreshData() {
|
|
3617
|
-
if (refreshDataInFlight) {
|
|
3618
|
-
refreshDataQueued = true;
|
|
3619
|
-
return;
|
|
3620
|
-
}
|
|
3621
|
-
refreshDataInFlight = true;
|
|
3622
|
-
try {
|
|
3623
|
-
const res = await fetch('/api/data');
|
|
3624
|
-
D = await res.json();
|
|
3605
|
+
function setLiveDot(color, title) {
|
|
3606
|
+
const dot = document.getElementById('liveDot');
|
|
3607
|
+
dot.style.background = color;
|
|
3608
|
+
dot.title = title;
|
|
3609
|
+
}
|
|
3610
|
+
|
|
3611
|
+
function scheduleRefreshData(delayMs = 120) {
|
|
3612
|
+
if (refreshDataTimer) return;
|
|
3613
|
+
refreshDataTimer = setTimeout(() => {
|
|
3614
|
+
refreshDataTimer = null;
|
|
3615
|
+
void refreshData();
|
|
3616
|
+
}, delayMs);
|
|
3617
|
+
}
|
|
3618
|
+
|
|
3619
|
+
async function refreshData() {
|
|
3620
|
+
if (refreshDataInFlight) {
|
|
3621
|
+
refreshDataQueued = true;
|
|
3622
|
+
return;
|
|
3623
|
+
}
|
|
3624
|
+
refreshDataInFlight = true;
|
|
3625
|
+
try {
|
|
3626
|
+
const res = await fetch('/api/data');
|
|
3627
|
+
D = await res.json();
|
|
3625
3628
|
|
|
3626
3629
|
// Update header timestamp
|
|
3627
3630
|
document.getElementById('scannedAt').textContent =
|
|
3628
3631
|
new Date(D.scannedAt).toLocaleTimeString();
|
|
3629
|
-
|
|
3630
|
-
renderStats();
|
|
3631
|
-
renderSidebar();
|
|
3632
|
-
updateAgentBtn();
|
|
3633
|
-
updateAgentRightsInfo();
|
|
3634
|
-
|
|
3635
|
-
// Re-render drill-down or re-open panel
|
|
3636
|
-
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
3632
|
+
|
|
3633
|
+
renderStats();
|
|
3634
|
+
renderSidebar();
|
|
3635
|
+
updateAgentBtn();
|
|
3636
|
+
updateAgentRightsInfo();
|
|
3637
|
+
|
|
3638
|
+
// Re-render drill-down or re-open panel
|
|
3639
|
+
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
3637
3640
|
if (panelOpen && activePanelKey) {
|
|
3638
3641
|
if (drillFeatureKey === '__unmapped__') {
|
|
3639
3642
|
renderContent(); // already routes to renderUnmappedDetail
|
|
3640
3643
|
} else if (view === 'features' && D.features) {
|
|
3641
3644
|
openFeaturePanel(activePanelKey);
|
|
3642
|
-
} else {
|
|
3643
|
-
const m = D.modules.find(m => m.id === activePanelKey);
|
|
3644
|
-
if (m) openModulePanel(m);
|
|
3645
|
-
else closePanel();
|
|
3646
|
-
}
|
|
3647
|
-
} else {
|
|
3648
|
-
renderContent();
|
|
3649
|
-
}
|
|
3650
|
-
|
|
3651
|
-
// Brief green flash on the dot to signal fresh data
|
|
3652
|
-
setLiveDot('#3fb950', 'Live — обновлено только что');
|
|
3653
|
-
setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
|
|
3654
|
-
void loadAgentState();
|
|
3655
|
-
} catch (err) {
|
|
3656
|
-
console.error('Refresh failed:', err);
|
|
3657
|
-
} finally {
|
|
3658
|
-
refreshDataInFlight = false;
|
|
3659
|
-
if (refreshDataQueued) {
|
|
3660
|
-
refreshDataQueued = false;
|
|
3661
|
-
scheduleRefreshData(120);
|
|
3662
|
-
}
|
|
3663
|
-
}
|
|
3664
|
-
}
|
|
3665
|
-
|
|
3666
|
-
function connectSSE() {
|
|
3667
|
-
const es = new EventSource('/api/events');
|
|
3668
|
-
|
|
3669
|
-
es.onopen = () => {
|
|
3670
|
-
setLiveDot('#3fb950', 'Live — автообновление включено');
|
|
3671
|
-
void loadAgentState();
|
|
3672
|
-
};
|
|
3673
|
-
|
|
3674
|
-
es.addEventListener('data-updated', () => {
|
|
3675
|
-
setLiveDot('#e3b341', 'Обновляю данные…');
|
|
3676
|
-
scheduleRefreshData();
|
|
3677
|
-
});
|
|
3678
|
-
|
|
3679
|
-
es.addEventListener('agent-queue-updated', (e) => {
|
|
3680
|
-
const payload = JSON.parse(e.data || '{}');
|
|
3681
|
-
agentQueueState = Array.isArray(payload.queue) ? payload.queue : [];
|
|
3682
|
-
refreshPathActivityFromState();
|
|
3683
|
-
updateQueueBadge(agentQueueState.length);
|
|
3684
|
-
renderContent();
|
|
3685
|
-
});
|
|
3686
|
-
|
|
3687
|
-
es.addEventListener('agent-run-created', (e) => {
|
|
3688
|
-
const { run } = JSON.parse(e.data || '{}');
|
|
3689
|
-
upsertRunState(run);
|
|
3690
|
-
});
|
|
3691
|
-
|
|
3692
|
-
es.addEventListener('agent-run-updated', (e) => {
|
|
3693
|
-
const { run } = JSON.parse(e.data || '{}');
|
|
3694
|
-
if (!run) return;
|
|
3695
|
-
if (run.runId && agentQueueState.length > 0) {
|
|
3696
|
-
agentQueueState = agentQueueState.filter((q) => q.runId !== run.runId || run.phase === 'queued');
|
|
3697
|
-
updateQueueBadge(agentQueueState.length);
|
|
3698
|
-
}
|
|
3699
|
-
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
3700
|
-
agentActiveRun = run;
|
|
3701
|
-
const startedPaths = getTaskFilePaths(run.task, run.featureKey, run.filePath, run.selectedFilePaths);
|
|
3702
|
-
startedPaths.forEach((p) => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3703
|
-
renderContent();
|
|
3704
|
-
}
|
|
3705
|
-
updateSessionFromRun(run);
|
|
3706
|
-
});
|
|
3707
|
-
|
|
3708
|
-
es.addEventListener('agent-run-finished', (e) => {
|
|
3709
|
-
const { run } = JSON.parse(e.data || '{}');
|
|
3710
|
-
if (!run) return;
|
|
3711
|
-
updateSessionFromRun(run);
|
|
3712
|
-
if (Array.isArray(run.fileOutcomes) && run.fileOutcomes.length > 0) {
|
|
3713
|
-
lastRunSummary = run;
|
|
3714
|
-
}
|
|
3715
|
-
syncSummaryForActiveSession();
|
|
3716
|
-
refreshPathActivityFromState();
|
|
3717
|
-
renderContent();
|
|
3718
|
-
});
|
|
3719
|
-
|
|
3720
|
-
es.addEventListener('agent-queued', (e) => {
|
|
3721
|
-
const { runId, queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3722
|
-
updateQueueBadge(queueLength);
|
|
3723
|
-
document.getElementById('agentPanel').classList.add('open');
|
|
3724
|
-
document.getElementById('termBtn').classList.add('term-active');
|
|
3725
|
-
// Track queued paths for spinner
|
|
3726
|
-
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
3727
|
-
renderContent();
|
|
3728
|
-
// Append queue notification to the currently running session (or active)
|
|
3729
|
-
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3730
|
-
if (targetId) {
|
|
3731
|
-
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
3732
|
-
}
|
|
3733
|
-
void loadAgentState();
|
|
3734
|
-
});
|
|
3735
|
-
|
|
3736
|
-
es.addEventListener('agent-started', (e) => {
|
|
3737
|
-
setAgentRunning(true);
|
|
3738
|
-
const { runId, title, queueLength = 0, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3739
|
-
updateQueueBadge(queueLength);
|
|
3740
|
-
currentRunId = runId || null;
|
|
3741
|
-
// Move paths from queued → running (current task)
|
|
3742
|
-
const startedPaths = getTaskFilePaths(task, featureKey, filePath, selectedFilePaths);
|
|
3743
|
-
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3744
|
-
renderContent();
|
|
3745
|
-
// Close previous session (queue case: agent-done not fired between tasks)
|
|
3746
|
-
if (runningSessionId) {
|
|
3747
|
-
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
3748
|
-
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
3749
|
-
}
|
|
3750
|
-
const id = runId ? ensureSessionForRun({ runId, title, phase: 'running' }, true) : createSession(title);
|
|
3751
|
-
runningSessionId = id;
|
|
3752
|
-
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
3753
|
-
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
3754
|
-
});
|
|
3755
|
-
|
|
3756
|
-
es.addEventListener('agent-output', (e) => {
|
|
3757
|
-
const { runId, line, isError, isDim } = JSON.parse(e.data);
|
|
3758
|
-
let targetId = runningSessionId;
|
|
3759
|
-
if (runId) {
|
|
3760
|
-
targetId = runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'running' });
|
|
3761
|
-
if (targetId) runningSessionId = targetId;
|
|
3762
|
-
}
|
|
3763
|
-
if (targetId) {
|
|
3764
|
-
appendToSession(targetId, line, !!isError, !!isDim);
|
|
3765
|
-
if (activeSessionId === targetId) {
|
|
3766
|
-
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
3767
|
-
}
|
|
3768
|
-
} else {
|
|
3769
|
-
appendTerminalLine(line, !!isError, !!isDim);
|
|
3770
|
-
}
|
|
3645
|
+
} else {
|
|
3646
|
+
const m = D.modules.find(m => m.id === activePanelKey);
|
|
3647
|
+
if (m) openModulePanel(m);
|
|
3648
|
+
else closePanel();
|
|
3649
|
+
}
|
|
3650
|
+
} else {
|
|
3651
|
+
renderContent();
|
|
3652
|
+
}
|
|
3653
|
+
|
|
3654
|
+
// Brief green flash on the dot to signal fresh data
|
|
3655
|
+
setLiveDot('#3fb950', 'Live — обновлено только что');
|
|
3656
|
+
setTimeout(() => setLiveDot('#3fb950', 'Live — автообновление включено'), 1500);
|
|
3657
|
+
void loadAgentState();
|
|
3658
|
+
} catch (err) {
|
|
3659
|
+
console.error('Refresh failed:', err);
|
|
3660
|
+
} finally {
|
|
3661
|
+
refreshDataInFlight = false;
|
|
3662
|
+
if (refreshDataQueued) {
|
|
3663
|
+
refreshDataQueued = false;
|
|
3664
|
+
scheduleRefreshData(120);
|
|
3665
|
+
}
|
|
3666
|
+
}
|
|
3667
|
+
}
|
|
3668
|
+
|
|
3669
|
+
function connectSSE() {
|
|
3670
|
+
const es = new EventSource('/api/events');
|
|
3671
|
+
|
|
3672
|
+
es.onopen = () => {
|
|
3673
|
+
setLiveDot('#3fb950', 'Live — автообновление включено');
|
|
3674
|
+
void loadAgentState();
|
|
3675
|
+
};
|
|
3676
|
+
|
|
3677
|
+
es.addEventListener('data-updated', () => {
|
|
3678
|
+
setLiveDot('#e3b341', 'Обновляю данные…');
|
|
3679
|
+
scheduleRefreshData();
|
|
3680
|
+
});
|
|
3681
|
+
|
|
3682
|
+
es.addEventListener('agent-queue-updated', (e) => {
|
|
3683
|
+
const payload = JSON.parse(e.data || '{}');
|
|
3684
|
+
agentQueueState = Array.isArray(payload.queue) ? payload.queue : [];
|
|
3685
|
+
refreshPathActivityFromState();
|
|
3686
|
+
updateQueueBadge(agentQueueState.length);
|
|
3687
|
+
renderContent();
|
|
3688
|
+
});
|
|
3689
|
+
|
|
3690
|
+
es.addEventListener('agent-run-created', (e) => {
|
|
3691
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3692
|
+
upsertRunState(run);
|
|
3693
|
+
});
|
|
3694
|
+
|
|
3695
|
+
es.addEventListener('agent-run-updated', (e) => {
|
|
3696
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3697
|
+
if (!run) return;
|
|
3698
|
+
if (run.runId && agentQueueState.length > 0) {
|
|
3699
|
+
agentQueueState = agentQueueState.filter((q) => q.runId !== run.runId || run.phase === 'queued');
|
|
3700
|
+
updateQueueBadge(agentQueueState.length);
|
|
3701
|
+
}
|
|
3702
|
+
if (['starting', 'running', 'validating'].includes(run.phase)) {
|
|
3703
|
+
agentActiveRun = run;
|
|
3704
|
+
const startedPaths = getTaskFilePaths(run.task, run.featureKey, run.filePath, run.selectedFilePaths);
|
|
3705
|
+
startedPaths.forEach((p) => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3706
|
+
renderContent();
|
|
3707
|
+
}
|
|
3708
|
+
updateSessionFromRun(run);
|
|
3709
|
+
});
|
|
3710
|
+
|
|
3711
|
+
es.addEventListener('agent-run-finished', (e) => {
|
|
3712
|
+
const { run } = JSON.parse(e.data || '{}');
|
|
3713
|
+
if (!run) return;
|
|
3714
|
+
updateSessionFromRun(run);
|
|
3715
|
+
if (Array.isArray(run.fileOutcomes) && run.fileOutcomes.length > 0) {
|
|
3716
|
+
lastRunSummary = run;
|
|
3717
|
+
}
|
|
3718
|
+
syncSummaryForActiveSession();
|
|
3719
|
+
refreshPathActivityFromState();
|
|
3720
|
+
renderContent();
|
|
3721
|
+
});
|
|
3722
|
+
|
|
3723
|
+
es.addEventListener('agent-queued', (e) => {
|
|
3724
|
+
const { runId, queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3725
|
+
updateQueueBadge(queueLength);
|
|
3726
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
3727
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
3728
|
+
// Track queued paths for spinner
|
|
3729
|
+
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
3730
|
+
renderContent();
|
|
3731
|
+
// Append queue notification to the currently running session (or active)
|
|
3732
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3733
|
+
if (targetId) {
|
|
3734
|
+
appendToSession(targetId, `📋 В очереди (${queueLength}): ${title}`, false);
|
|
3735
|
+
}
|
|
3736
|
+
void loadAgentState();
|
|
3737
|
+
});
|
|
3738
|
+
|
|
3739
|
+
es.addEventListener('agent-started', (e) => {
|
|
3740
|
+
setAgentRunning(true);
|
|
3741
|
+
const { runId, title, queueLength = 0, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
3742
|
+
updateQueueBadge(queueLength);
|
|
3743
|
+
currentRunId = runId || null;
|
|
3744
|
+
// Move paths from queued → running (current task)
|
|
3745
|
+
const startedPaths = getTaskFilePaths(task, featureKey, filePath, selectedFilePaths);
|
|
3746
|
+
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
3747
|
+
renderContent();
|
|
3748
|
+
// Close previous session (queue case: agent-done not fired between tasks)
|
|
3749
|
+
if (runningSessionId) {
|
|
3750
|
+
const prev = consoleSessions.find(s => s.id === runningSessionId);
|
|
3751
|
+
if (prev && prev.status === 'running') updateSessionStatus(runningSessionId, 'ok');
|
|
3752
|
+
}
|
|
3753
|
+
const id = runId ? ensureSessionForRun({ runId, title, phase: 'running' }, true) : createSession(title);
|
|
3754
|
+
runningSessionId = id;
|
|
3755
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
3756
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
3771
3757
|
});
|
|
3772
3758
|
|
|
3773
|
-
es.addEventListener('agent-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3759
|
+
es.addEventListener('agent-output', (e) => {
|
|
3760
|
+
const { runId, line, isError, isDim } = JSON.parse(e.data);
|
|
3761
|
+
let targetId = runningSessionId;
|
|
3762
|
+
if (runId) {
|
|
3763
|
+
targetId = runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'running' });
|
|
3764
|
+
if (targetId) runningSessionId = targetId;
|
|
3765
|
+
}
|
|
3766
|
+
if (targetId) {
|
|
3767
|
+
appendToSession(targetId, line, !!isError, !!isDim);
|
|
3768
|
+
if (activeSessionId === targetId) {
|
|
3769
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
3770
|
+
}
|
|
3771
|
+
} else {
|
|
3772
|
+
appendTerminalLine(line, !!isError, !!isDim);
|
|
3773
|
+
}
|
|
3774
|
+
});
|
|
3775
|
+
|
|
3776
|
+
es.addEventListener('agent-done', () => {
|
|
3777
|
+
setAgentRunning(false);
|
|
3778
|
+
if (agentQueueState.length === 0) updateQueueBadge(0);
|
|
3779
|
+
agentRunningPaths.clear();
|
|
3780
|
+
if (runningSessionId) {
|
|
3781
|
+
updateSessionStatus(runningSessionId, 'ok');
|
|
3782
|
+
if (activeSessionId === runningSessionId) {
|
|
3783
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
3784
|
+
}
|
|
3782
3785
|
runningSessionId = null;
|
|
3783
3786
|
}
|
|
3784
3787
|
renderContent();
|
|
3785
3788
|
});
|
|
3786
3789
|
|
|
3787
|
-
es.addEventListener('agent-summary', (e) => {
|
|
3788
|
-
const {
|
|
3789
|
-
runId,
|
|
3790
|
-
fileOutcomes = [],
|
|
3791
|
-
validationStats = null,
|
|
3792
|
-
passed = 0, failed = 0, files = [],
|
|
3793
|
-
testedFileCount = files.length,
|
|
3794
|
-
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
3795
|
-
failedFileCount = failed > 0 ? 1 : 0,
|
|
3796
|
-
autoFixQueued = false,
|
|
3797
|
-
} = JSON.parse(e.data);
|
|
3798
|
-
const coverageOk = !validationStats || ((validationStats.notCovered || 0) === 0 && (validationStats.blocked || 0) === 0);
|
|
3799
|
-
const allOk = failed === 0 && coverageOk;
|
|
3790
|
+
es.addEventListener('agent-summary', (e) => {
|
|
3791
|
+
const {
|
|
3792
|
+
runId,
|
|
3793
|
+
fileOutcomes = [],
|
|
3794
|
+
validationStats = null,
|
|
3795
|
+
passed = 0, failed = 0, files = [],
|
|
3796
|
+
testedFileCount = files.length,
|
|
3797
|
+
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
3798
|
+
failedFileCount = failed > 0 ? 1 : 0,
|
|
3799
|
+
autoFixQueued = false,
|
|
3800
|
+
} = JSON.parse(e.data);
|
|
3801
|
+
const coverageOk = !validationStats || ((validationStats.notCovered || 0) === 0 && (validationStats.blocked || 0) === 0);
|
|
3802
|
+
const allOk = failed === 0 && coverageOk;
|
|
3800
3803
|
const box = document.createElement('div');
|
|
3801
3804
|
box.style.cssText = `
|
|
3802
3805
|
margin: 10px 0 4px;
|
|
@@ -3813,37 +3816,37 @@ function connectSSE() {
|
|
|
3813
3816
|
<div style="font-size:11px;color:var(--muted);margin-top:4px">
|
|
3814
3817
|
Файлы: ${testedFileCount} • passed: ${passedFileCount} • failed: ${failedFileCount}
|
|
3815
3818
|
</div>
|
|
3816
|
-
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3817
|
-
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3818
|
-
</div>
|
|
3819
|
-
${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>` : ''}
|
|
3820
|
-
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
3821
|
-
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
3822
|
-
`;
|
|
3823
|
-
if (fileOutcomes.length > 0) {
|
|
3824
|
-
lastRunSummary = { runId, fileOutcomes, validationStats };
|
|
3825
|
-
}
|
|
3826
|
-
syncSummaryForActiveSession();
|
|
3827
|
-
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3828
|
-
if (targetId) {
|
|
3829
|
-
appendToSession(targetId, box);
|
|
3830
|
-
// Mark session status from test results immediately
|
|
3831
|
-
if (runningSessionId === targetId) {
|
|
3832
|
-
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
3819
|
+
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3820
|
+
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3821
|
+
</div>
|
|
3822
|
+
${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>` : ''}
|
|
3823
|
+
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
3824
|
+
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
3825
|
+
`;
|
|
3826
|
+
if (fileOutcomes.length > 0) {
|
|
3827
|
+
lastRunSummary = { runId, fileOutcomes, validationStats };
|
|
3828
|
+
}
|
|
3829
|
+
syncSummaryForActiveSession();
|
|
3830
|
+
const targetId = runId ? (runSessionMap.get(runId) || runningSessionId || activeSessionId) : (runningSessionId || activeSessionId);
|
|
3831
|
+
if (targetId) {
|
|
3832
|
+
appendToSession(targetId, box);
|
|
3833
|
+
// Mark session status from test results immediately
|
|
3834
|
+
if (runningSessionId === targetId) {
|
|
3835
|
+
updateSessionStatus(targetId, failed > 0 ? 'error' : 'ok');
|
|
3833
3836
|
}
|
|
3834
3837
|
}
|
|
3835
3838
|
});
|
|
3836
3839
|
|
|
3837
|
-
es.addEventListener('agent-error', (e) => {
|
|
3838
|
-
const { runId, message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
3839
|
-
if (!runId) setAgentRunning(false);
|
|
3840
|
-
agentRunningPaths.clear();
|
|
3841
|
-
agentQueuedPaths.clear();
|
|
3842
|
-
renderContent();
|
|
3843
|
-
|
|
3844
|
-
// Build error node (plain text or auth/install box)
|
|
3845
|
-
let node = null;
|
|
3846
|
-
if (authRequired || notInstalled) {
|
|
3840
|
+
es.addEventListener('agent-error', (e) => {
|
|
3841
|
+
const { runId, message, authRequired, notInstalled } = JSON.parse(e.data);
|
|
3842
|
+
if (!runId) setAgentRunning(false);
|
|
3843
|
+
agentRunningPaths.clear();
|
|
3844
|
+
agentQueuedPaths.clear();
|
|
3845
|
+
renderContent();
|
|
3846
|
+
|
|
3847
|
+
// Build error node (plain text or auth/install box)
|
|
3848
|
+
let node = null;
|
|
3849
|
+
if (authRequired || notInstalled) {
|
|
3847
3850
|
node = document.createElement('div');
|
|
3848
3851
|
node.style.cssText = 'margin:10px 0 4px;padding:10px 14px;border-radius:8px;border:1px solid var(--red);background:#2a0d0d;font-family:inherit;';
|
|
3849
3852
|
node.innerHTML = `
|
|
@@ -3851,14 +3854,14 @@ function connectSSE() {
|
|
|
3851
3854
|
${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>` : ''}
|
|
3852
3855
|
${notInstalled ? `<div style="margin-top:6px;font-size:11px;color:var(--dim)">После установки перезапусти viberadar</div>` : ''}
|
|
3853
3856
|
`;
|
|
3854
|
-
}
|
|
3855
|
-
|
|
3856
|
-
// If no session exists yet (startup check fires before any run), create one
|
|
3857
|
-
const targetId = (runId ? (runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'failed' })) : runningSessionId) || (() => {
|
|
3858
|
-
if (authRequired || notInstalled) {
|
|
3859
|
-
const id = createSession('⚠️ Проверка агента', 'error');
|
|
3860
|
-
return id;
|
|
3861
|
-
}
|
|
3857
|
+
}
|
|
3858
|
+
|
|
3859
|
+
// If no session exists yet (startup check fires before any run), create one
|
|
3860
|
+
const targetId = (runId ? (runSessionMap.get(runId) || ensureSessionForRun({ runId, title: `Run ${runId}`, phase: 'failed' })) : runningSessionId) || (() => {
|
|
3861
|
+
if (authRequired || notInstalled) {
|
|
3862
|
+
const id = createSession('⚠️ Проверка агента', 'error');
|
|
3863
|
+
return id;
|
|
3864
|
+
}
|
|
3862
3865
|
return activeSessionId;
|
|
3863
3866
|
})();
|
|
3864
3867
|
|
|
@@ -3869,12 +3872,12 @@ function connectSSE() {
|
|
|
3869
3872
|
appendToSession(targetId, '❌ ' + (message || 'Ошибка агента'), true);
|
|
3870
3873
|
}
|
|
3871
3874
|
updateSessionStatus(targetId, 'error');
|
|
3872
|
-
if (activeSessionId === targetId) {
|
|
3873
|
-
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3874
|
-
}
|
|
3875
|
-
}
|
|
3876
|
-
if (!runId && runningSessionId) runningSessionId = null;
|
|
3877
|
-
});
|
|
3875
|
+
if (activeSessionId === targetId) {
|
|
3876
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
3877
|
+
}
|
|
3878
|
+
}
|
|
3879
|
+
if (!runId && runningSessionId) runningSessionId = null;
|
|
3880
|
+
});
|
|
3878
3881
|
|
|
3879
3882
|
es.addEventListener('tests-started', (e) => {
|
|
3880
3883
|
const { testType, count } = JSON.parse(e.data);
|
|
@@ -3942,24 +3945,24 @@ function connectSSE() {
|
|
|
3942
3945
|
}
|
|
3943
3946
|
});
|
|
3944
3947
|
|
|
3945
|
-
es.onerror = () => {
|
|
3946
|
-
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
3947
|
-
es.close();
|
|
3948
|
-
setTimeout(connectSSE, 3000);
|
|
3949
|
-
};
|
|
3950
|
-
}
|
|
3951
|
-
|
|
3952
|
-
init().then(async () => {
|
|
3953
|
-
restoreSessions();
|
|
3954
|
-
await loadAgentState();
|
|
3955
|
-
connectSSE();
|
|
3956
|
-
applyTerminalFiltersFromUi();
|
|
3957
|
-
updateToolbarButtonsState();
|
|
3958
|
-
// Sync content padding with terminal panel open state automatically
|
|
3959
|
-
new MutationObserver(() => {
|
|
3960
|
-
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
3961
|
-
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
3962
|
-
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
3948
|
+
es.onerror = () => {
|
|
3949
|
+
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
3950
|
+
es.close();
|
|
3951
|
+
setTimeout(connectSSE, 3000);
|
|
3952
|
+
};
|
|
3953
|
+
}
|
|
3954
|
+
|
|
3955
|
+
init().then(async () => {
|
|
3956
|
+
restoreSessions();
|
|
3957
|
+
await loadAgentState();
|
|
3958
|
+
connectSSE();
|
|
3959
|
+
applyTerminalFiltersFromUi();
|
|
3960
|
+
updateToolbarButtonsState();
|
|
3961
|
+
// Sync content padding with terminal panel open state automatically
|
|
3962
|
+
new MutationObserver(() => {
|
|
3963
|
+
const isOpen = document.getElementById('agentPanel').classList.contains('open');
|
|
3964
|
+
document.getElementById('content').classList.toggle('panel-open', isOpen);
|
|
3965
|
+
}).observe(document.getElementById('agentPanel'), { attributes: true, attributeFilter: ['class'] });
|
|
3963
3966
|
});
|
|
3964
3967
|
</script>
|
|
3965
3968
|
</body>
|