viberadar 0.3.55 → 0.3.57
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/README.md +54 -22
- package/dist/scanner/index.d.ts +33 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +162 -1
- package/dist/scanner/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +112 -10
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +752 -240
- package/package.json +4 -2
package/dist/ui/dashboard.html
CHANGED
|
@@ -42,18 +42,18 @@
|
|
|
42
42
|
flex-shrink: 0;
|
|
43
43
|
z-index: 10;
|
|
44
44
|
}
|
|
45
|
-
header h1 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; }
|
|
46
|
-
.header-project { margin-left: auto; font-size: 13px; color: var(--muted); }
|
|
47
|
-
.header-time { font-size: 12px; color: var(--dim); }
|
|
48
|
-
.header-agent-rights {
|
|
49
|
-
font-size: 11px;
|
|
50
|
-
color: var(--muted);
|
|
51
|
-
padding: 2px 8px;
|
|
52
|
-
border: 1px solid var(--border);
|
|
53
|
-
border-radius: 999px;
|
|
54
|
-
background: var(--bg);
|
|
55
|
-
white-space: nowrap;
|
|
56
|
-
}
|
|
45
|
+
header h1 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; }
|
|
46
|
+
.header-project { margin-left: auto; font-size: 13px; color: var(--muted); }
|
|
47
|
+
.header-time { font-size: 12px; color: var(--dim); }
|
|
48
|
+
.header-agent-rights {
|
|
49
|
+
font-size: 11px;
|
|
50
|
+
color: var(--muted);
|
|
51
|
+
padding: 2px 8px;
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
border-radius: 999px;
|
|
54
|
+
background: var(--bg);
|
|
55
|
+
white-space: nowrap;
|
|
56
|
+
}
|
|
57
57
|
|
|
58
58
|
/* ── Run All Tests button ────────────────────────────────────────────────── */
|
|
59
59
|
#runAllBtn {
|
|
@@ -168,6 +168,32 @@
|
|
|
168
168
|
.stat-value { font-size: 20px; font-weight: 700; color: var(--blue); }
|
|
169
169
|
.stat-label { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; }
|
|
170
170
|
|
|
171
|
+
|
|
172
|
+
.observability-panel {
|
|
173
|
+
background: var(--bg-card);
|
|
174
|
+
border: 1px solid var(--border);
|
|
175
|
+
border-radius: 8px;
|
|
176
|
+
padding: 14px;
|
|
177
|
+
margin-bottom: 12px;
|
|
178
|
+
}
|
|
179
|
+
.observability-title { font-size: 14px; font-weight: 700; margin-bottom: 8px; }
|
|
180
|
+
.obs-metrics { display:grid; grid-template-columns: repeat(auto-fit,minmax(160px,1fr)); gap:8px; margin-bottom:10px; }
|
|
181
|
+
.obs-metric { background: var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px; }
|
|
182
|
+
.obs-metric-v { font-size:18px; font-weight:700; color: var(--blue); }
|
|
183
|
+
.obs-metric-l { font-size:10px; color: var(--muted); text-transform: uppercase; }
|
|
184
|
+
.obs-columns { display:grid; grid-template-columns: 1fr 1fr; gap:10px; }
|
|
185
|
+
.obs-list { background: var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px; }
|
|
186
|
+
.obs-list h4 { font-size:12px; margin-bottom:6px; color: var(--text); }
|
|
187
|
+
.obs-row { font-size:12px; color: var(--muted); padding:4px 0; border-bottom:1px dashed var(--border); }
|
|
188
|
+
.obs-row:last-child { border-bottom:none; }
|
|
189
|
+
.obs-priority-high { color: var(--red); }
|
|
190
|
+
.obs-priority-medium { color: var(--yellow); }
|
|
191
|
+
.obs-priority-low { color: var(--green); }
|
|
192
|
+
.obs-catalog { margin-top:10px; background: var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px; }
|
|
193
|
+
.obs-catalog h4 { font-size:12px; margin-bottom:6px; }
|
|
194
|
+
.obs-cat-row { display:grid; grid-template-columns: 1.8fr .6fr .8fr .7fr .8fr 1fr; gap:8px; font-size:11px; color:var(--muted); padding:5px 0; border-bottom:1px dashed var(--border); }
|
|
195
|
+
.obs-cat-row.head { color: var(--text); font-weight:600; text-transform:uppercase; font-size:10px; }
|
|
196
|
+
|
|
171
197
|
/* ── Layout ──────────────────────────────────────────────────────────────── */
|
|
172
198
|
.layout { display: flex; flex: 1; overflow: hidden; }
|
|
173
199
|
|
|
@@ -183,6 +209,30 @@
|
|
|
183
209
|
gap: 14px;
|
|
184
210
|
}
|
|
185
211
|
|
|
212
|
+
.mode-switch {
|
|
213
|
+
display: flex;
|
|
214
|
+
flex-direction: column;
|
|
215
|
+
gap: 6px;
|
|
216
|
+
}
|
|
217
|
+
.mode-switch-btn {
|
|
218
|
+
display: flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
justify-content: space-between;
|
|
221
|
+
gap: 8px;
|
|
222
|
+
width: 100%;
|
|
223
|
+
padding: 8px 10px;
|
|
224
|
+
border: 1px solid var(--border);
|
|
225
|
+
border-radius: 6px;
|
|
226
|
+
background: var(--bg);
|
|
227
|
+
color: var(--muted);
|
|
228
|
+
font-size: 12px;
|
|
229
|
+
cursor: pointer;
|
|
230
|
+
transition: border-color 0.1s, background 0.1s, color 0.1s;
|
|
231
|
+
}
|
|
232
|
+
.mode-switch-btn:hover { background: var(--bg-hover); color: var(--text); }
|
|
233
|
+
.mode-switch-btn.active { border-color: var(--blue); color: var(--text); }
|
|
234
|
+
.mode-switch-hint { font-size: 10px; color: var(--dim); }
|
|
235
|
+
|
|
186
236
|
.view-tabs {
|
|
187
237
|
display: flex;
|
|
188
238
|
background: var(--bg);
|
|
@@ -246,6 +296,33 @@
|
|
|
246
296
|
.content { flex: 1; overflow-y: auto; padding: 18px 20px; transition: padding-bottom 0.25s ease; }
|
|
247
297
|
.content.panel-open { padding-bottom: 300px; }
|
|
248
298
|
|
|
299
|
+
.onboarding-block {
|
|
300
|
+
background: linear-gradient(135deg, rgba(88,166,255,0.12), rgba(63,185,80,0.08));
|
|
301
|
+
border: 1px solid var(--border);
|
|
302
|
+
border-radius: 10px;
|
|
303
|
+
padding: 14px 16px;
|
|
304
|
+
margin-bottom: 14px;
|
|
305
|
+
}
|
|
306
|
+
.onboarding-block h3 { font-size: 14px; margin-bottom: 6px; }
|
|
307
|
+
.onboarding-block p { font-size: 12px; color: var(--muted); line-height: 1.5; }
|
|
308
|
+
|
|
309
|
+
.obs-grid {
|
|
310
|
+
display: grid;
|
|
311
|
+
grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
|
|
312
|
+
gap: 12px;
|
|
313
|
+
}
|
|
314
|
+
.obs-card {
|
|
315
|
+
background: var(--bg-card);
|
|
316
|
+
border: 1px solid var(--border);
|
|
317
|
+
border-radius: 8px;
|
|
318
|
+
padding: 14px;
|
|
319
|
+
}
|
|
320
|
+
.obs-title { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 10px; }
|
|
321
|
+
.obs-value { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
|
|
322
|
+
.obs-sub { font-size: 12px; color: var(--muted); line-height: 1.4; }
|
|
323
|
+
.obs-list { display: flex; flex-direction: column; gap: 6px; }
|
|
324
|
+
.obs-list-item { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; }
|
|
325
|
+
|
|
249
326
|
/* ── Feature cards ───────────────────────────────────────────────────────── */
|
|
250
327
|
.features-grid {
|
|
251
328
|
display: grid;
|
|
@@ -548,6 +625,13 @@
|
|
|
548
625
|
.file-row:hover { background: var(--bg-card); }
|
|
549
626
|
.file-row.active { background: var(--bg-hover); border-left: 2px solid var(--blue); padding-left: 8px; }
|
|
550
627
|
.file-row-icon { font-size: 12px; flex-shrink: 0; }
|
|
628
|
+
.file-row-select {
|
|
629
|
+
width: 14px;
|
|
630
|
+
height: 14px;
|
|
631
|
+
accent-color: var(--blue);
|
|
632
|
+
cursor: pointer;
|
|
633
|
+
flex-shrink: 0;
|
|
634
|
+
}
|
|
551
635
|
.file-row-name { font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; min-width: 0; }
|
|
552
636
|
.file-row-dir { font-size: 11px; color: var(--dim); margin-left: auto; white-space: nowrap; flex-shrink: 0; }
|
|
553
637
|
.file-row-agent-btn {
|
|
@@ -738,11 +822,66 @@
|
|
|
738
822
|
white-space: nowrap;
|
|
739
823
|
}
|
|
740
824
|
.file-row-more-item:hover { background: var(--border); }
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
825
|
+
|
|
826
|
+
.bulk-actions {
|
|
827
|
+
display: flex;
|
|
828
|
+
align-items: center;
|
|
829
|
+
gap: 8px;
|
|
830
|
+
margin: 0 0 10px;
|
|
831
|
+
padding: 8px;
|
|
832
|
+
background: var(--bg-card);
|
|
833
|
+
border: 1px solid var(--border);
|
|
834
|
+
border-radius: 8px;
|
|
835
|
+
flex-wrap: wrap;
|
|
836
|
+
}
|
|
837
|
+
.bulk-actions-count { font-size: 12px; color: var(--muted); }
|
|
838
|
+
.bulk-actions-btn {
|
|
839
|
+
padding: 6px 10px;
|
|
840
|
+
border-radius: 6px;
|
|
841
|
+
border: 1px solid var(--border);
|
|
842
|
+
background: var(--bg);
|
|
843
|
+
color: var(--text);
|
|
844
|
+
font-size: 12px;
|
|
845
|
+
cursor: pointer;
|
|
846
|
+
}
|
|
847
|
+
.bulk-actions-btn.primary {
|
|
848
|
+
border-color: var(--accent);
|
|
849
|
+
color: var(--accent);
|
|
850
|
+
}
|
|
851
|
+
.bulk-actions-btn:hover:not(:disabled) { background: var(--bg-hover); }
|
|
852
|
+
.bulk-actions-btn:disabled { opacity: 0.4; cursor: not-allowed; }
|
|
853
|
+
.feature-file-filters {
|
|
854
|
+
display: flex;
|
|
855
|
+
align-items: center;
|
|
856
|
+
gap: 8px;
|
|
857
|
+
margin: 0 0 10px;
|
|
858
|
+
}
|
|
859
|
+
.feature-filter-toggle {
|
|
860
|
+
display: inline-flex;
|
|
861
|
+
align-items: center;
|
|
862
|
+
gap: 8px;
|
|
863
|
+
padding: 6px 10px;
|
|
864
|
+
border-radius: 6px;
|
|
865
|
+
border: 1px solid var(--border);
|
|
866
|
+
background: var(--bg-card);
|
|
867
|
+
color: var(--muted);
|
|
868
|
+
font-size: 12px;
|
|
869
|
+
cursor: pointer;
|
|
870
|
+
user-select: none;
|
|
871
|
+
}
|
|
872
|
+
.feature-filter-toggle:hover { background: var(--bg-hover); color: var(--text); }
|
|
873
|
+
.feature-filter-toggle input {
|
|
874
|
+
width: 14px;
|
|
875
|
+
height: 14px;
|
|
876
|
+
accent-color: var(--blue);
|
|
877
|
+
cursor: pointer;
|
|
878
|
+
}
|
|
879
|
+
.feature-filter-meta { color: var(--dim); font-size: 11px; }
|
|
880
|
+
.agent-terminal {
|
|
881
|
+
flex: 1;
|
|
882
|
+
overflow-y: auto;
|
|
883
|
+
padding: 10px 16px;
|
|
884
|
+
font-family: 'Consolas', 'Menlo', 'Courier New', monospace;
|
|
746
885
|
font-size: 12px;
|
|
747
886
|
line-height: 1.5;
|
|
748
887
|
}
|
|
@@ -801,10 +940,10 @@
|
|
|
801
940
|
<header>
|
|
802
941
|
<span style="font-size:20px">🔭</span>
|
|
803
942
|
<h1>VibeRadar</h1>
|
|
804
|
-
<span class="header-project" id="projectName">—</span>
|
|
805
|
-
<span class="header-time" id="scannedAt"></span>
|
|
806
|
-
<span class="header-agent-rights" id="headerAgentRights" title="Права/режим выполнения агента">🔐 —</span>
|
|
807
|
-
<button id="runAllBtn" onclick="runAllTests()" title="Запустить все unit и integration тесты">▶ Все тесты</button>
|
|
943
|
+
<span class="header-project" id="projectName">—</span>
|
|
944
|
+
<span class="header-time" id="scannedAt"></span>
|
|
945
|
+
<span class="header-agent-rights" id="headerAgentRights" title="Права/режим выполнения агента">🔐 —</span>
|
|
946
|
+
<button id="runAllBtn" onclick="runAllTests()" title="Запустить все unit и integration тесты">▶ Все тесты</button>
|
|
808
947
|
<button id="termBtn" onclick="toggleAgentPanel()" title="Показать/скрыть терминал агента">📟 Terminal</button>
|
|
809
948
|
<div style="position:relative">
|
|
810
949
|
<button id="agentBtn" onclick="toggleAgentMenu()" title="Настройки агента">🤖 —</button>
|
|
@@ -843,6 +982,7 @@
|
|
|
843
982
|
|
|
844
983
|
<div class="layout">
|
|
845
984
|
<aside class="sidebar">
|
|
985
|
+
<div class="mode-switch" id="modeSwitch"></div>
|
|
846
986
|
<div class="view-tabs" id="viewTabs">
|
|
847
987
|
<div class="view-tab" data-view="features">Features</div>
|
|
848
988
|
<div class="view-tab" data-view="files">Files</div>
|
|
@@ -878,67 +1018,150 @@
|
|
|
878
1018
|
<script>
|
|
879
1019
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
880
1020
|
let D = null;
|
|
1021
|
+
let contextMode = 'qa';
|
|
881
1022
|
let view = 'features';
|
|
882
1023
|
let searchQuery = '';
|
|
883
1024
|
let activeTypes = new Set();
|
|
884
|
-
let activePanelKey = null;
|
|
885
|
-
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
1025
|
+
let activePanelKey = null;
|
|
1026
|
+
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
886
1027
|
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
1028
|
+
let showOnlyUntestedInFeature = false; // source tab in feature detail
|
|
1029
|
+
const selectedSourceFiles = new Set(); // normalized relative paths for batch actions
|
|
887
1030
|
let e2ePlan = null; // current E2E plan object
|
|
888
1031
|
let e2ePlanLoading = false;
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
return
|
|
901
|
-
}
|
|
902
|
-
|
|
903
|
-
function
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1032
|
+
const modeStore = {
|
|
1033
|
+
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1034
|
+
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1035
|
+
};
|
|
1036
|
+
|
|
1037
|
+
function getModeFromPath(pathname = window.location.pathname) {
|
|
1038
|
+
if (pathname.startsWith('/radar/observability')) return 'observability';
|
|
1039
|
+
return 'qa';
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function routePathForMode(mode) {
|
|
1043
|
+
return mode === 'observability' ? '/radar/observability' : '/radar/qa';
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function setModeRoute(mode, replace = false) {
|
|
1047
|
+
const target = routePathForMode(mode) + window.location.search + window.location.hash;
|
|
1048
|
+
if (replace) history.replaceState(null, '', target);
|
|
1049
|
+
else history.pushState(null, '', target);
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
function saveModeState(mode) {
|
|
1053
|
+
modeStore[mode] = {
|
|
1054
|
+
view,
|
|
1055
|
+
searchQuery,
|
|
1056
|
+
activeTypes: new Set(activeTypes),
|
|
1057
|
+
drillFeatureKey,
|
|
1058
|
+
drillTestType,
|
|
1059
|
+
activePanelKey,
|
|
1060
|
+
showOnlyUntestedInFeature,
|
|
1061
|
+
};
|
|
918
1062
|
}
|
|
919
1063
|
|
|
920
|
-
function
|
|
921
|
-
const
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
}
|
|
930
|
-
const params = new URLSearchParams(hash);
|
|
931
|
-
const featureKey = params.get('feature');
|
|
932
|
-
if (!featureKey) return;
|
|
933
|
-
const isKnownFeature = Array.isArray(D?.features) && D.features.some(f => f.key === featureKey);
|
|
934
|
-
if (featureKey === '__unmapped__' || isKnownFeature) {
|
|
935
|
-
view = 'features';
|
|
936
|
-
drillFeatureKey = featureKey;
|
|
937
|
-
drillTestType = null;
|
|
938
|
-
activePanelKey = null;
|
|
939
|
-
}
|
|
1064
|
+
function restoreModeState(mode) {
|
|
1065
|
+
const state = modeStore[mode];
|
|
1066
|
+
view = state.view;
|
|
1067
|
+
searchQuery = state.searchQuery;
|
|
1068
|
+
activeTypes = new Set(state.activeTypes);
|
|
1069
|
+
drillFeatureKey = state.drillFeatureKey;
|
|
1070
|
+
drillTestType = state.drillTestType;
|
|
1071
|
+
activePanelKey = state.activePanelKey;
|
|
1072
|
+
showOnlyUntestedInFeature = !!state.showOnlyUntestedInFeature;
|
|
940
1073
|
}
|
|
941
1074
|
|
|
1075
|
+
function switchMode(nextMode) {
|
|
1076
|
+
if (nextMode === contextMode) return;
|
|
1077
|
+
saveModeState(contextMode);
|
|
1078
|
+
contextMode = nextMode;
|
|
1079
|
+
restoreModeState(contextMode);
|
|
1080
|
+
if (contextMode === 'observability') {
|
|
1081
|
+
view = 'files';
|
|
1082
|
+
drillFeatureKey = null;
|
|
1083
|
+
drillTestType = null;
|
|
1084
|
+
activePanelKey = null;
|
|
1085
|
+
clearFeatureHash();
|
|
1086
|
+
}
|
|
1087
|
+
setModeRoute(contextMode);
|
|
1088
|
+
document.getElementById('searchInput').value = searchQuery;
|
|
1089
|
+
document.getElementById('panel').classList.remove('open');
|
|
1090
|
+
renderStats();
|
|
1091
|
+
renderSidebar();
|
|
1092
|
+
renderContent();
|
|
1093
|
+
}
|
|
1094
|
+
// ─── Run All Tests button ──────────────────────────────────────────────────────
|
|
1095
|
+
let runAllRunning = false;
|
|
1096
|
+
|
|
1097
|
+
function escapeHtml(text) {
|
|
1098
|
+
return String(text || '')
|
|
1099
|
+
.replace(/&/g, '&')
|
|
1100
|
+
.replace(/</g, '<')
|
|
1101
|
+
.replace(/>/g, '>');
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
|
|
1105
|
+
function clearFeatureHash() {
|
|
1106
|
+
if (!window.location.hash) return;
|
|
1107
|
+
history.replaceState(null, '', window.location.pathname + window.location.search);
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function buildFeatureTabUrl(featureKey) {
|
|
1111
|
+
const url = new URL(window.location.href);
|
|
1112
|
+
url.hash = `feature=${encodeURIComponent(featureKey)}`;
|
|
1113
|
+
return url.toString();
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
function openFeatureInNewTab(featureKey) {
|
|
1117
|
+
window.open(buildFeatureTabUrl(featureKey), '_blank', 'noopener');
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
function setFeatureDrill(featureKey, syncHash = true) {
|
|
1121
|
+
view = 'features';
|
|
1122
|
+
drillFeatureKey = featureKey;
|
|
1123
|
+
drillTestType = null;
|
|
1124
|
+
activePanelKey = null;
|
|
1125
|
+
selectedSourceFiles.clear();
|
|
1126
|
+
document.getElementById('panel').classList.remove('open');
|
|
1127
|
+
if (syncHash) {
|
|
1128
|
+
if (featureKey) window.location.hash = `feature=${encodeURIComponent(featureKey)}`;
|
|
1129
|
+
else clearFeatureHash();
|
|
1130
|
+
}
|
|
1131
|
+
renderContent();
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function selectedFilesForFeature(featureKey, visibleSourceFiles = null) {
|
|
1135
|
+
const visibleSet = visibleSourceFiles ? new Set(visibleSourceFiles.map(p => p.replace(/\\/g, '/'))) : null;
|
|
1136
|
+
return Array.from(selectedSourceFiles).filter((relPath) => {
|
|
1137
|
+
if (visibleSet && !visibleSet.has(relPath)) return false;
|
|
1138
|
+
const mod = D?.modules?.find((m) => m.relativePath.replace(/\\/g, '/') === relPath);
|
|
1139
|
+
return !!mod && mod.type !== 'test' && mod.featureKeys?.includes(featureKey);
|
|
1140
|
+
});
|
|
1141
|
+
}
|
|
1142
|
+
|
|
1143
|
+
function applyHashRoute() {
|
|
1144
|
+
const hash = (window.location.hash || '').replace(/^#/, '');
|
|
1145
|
+
if (!hash) {
|
|
1146
|
+
if (view === 'features' && drillFeatureKey) {
|
|
1147
|
+
drillFeatureKey = null;
|
|
1148
|
+
drillTestType = null;
|
|
1149
|
+
activePanelKey = null;
|
|
1150
|
+
}
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
const params = new URLSearchParams(hash);
|
|
1154
|
+
const featureKey = params.get('feature');
|
|
1155
|
+
if (!featureKey) return;
|
|
1156
|
+
const isKnownFeature = Array.isArray(D?.features) && D.features.some(f => f.key === featureKey);
|
|
1157
|
+
if (featureKey === '__unmapped__' || isKnownFeature) {
|
|
1158
|
+
view = 'features';
|
|
1159
|
+
drillFeatureKey = featureKey;
|
|
1160
|
+
drillTestType = null;
|
|
1161
|
+
activePanelKey = null;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
942
1165
|
function updateRunAllBtn(state) {
|
|
943
1166
|
const btn = document.getElementById('runAllBtn');
|
|
944
1167
|
if (!btn) return;
|
|
@@ -1167,10 +1390,13 @@ function setAgentRunning(val) {
|
|
|
1167
1390
|
}
|
|
1168
1391
|
|
|
1169
1392
|
// Returns array of normalized relative paths affected by a given task
|
|
1170
|
-
function getTaskFilePaths(task, featureKey, filePath) {
|
|
1393
|
+
function getTaskFilePaths(task, featureKey, filePath, selectedFilePaths) {
|
|
1171
1394
|
if ((task === 'write-tests-file' || task === 'fix-tests') && filePath) {
|
|
1172
1395
|
return [filePath.replace(/\\/g, '/')];
|
|
1173
1396
|
}
|
|
1397
|
+
if ((task === 'write-tests-selected' || task === 'refresh-tests-selected') && Array.isArray(selectedFilePaths)) {
|
|
1398
|
+
return selectedFilePaths.map(p => p.replace(/\\/g, '/'));
|
|
1399
|
+
}
|
|
1174
1400
|
if ((task === 'write-tests' || task === 'fix-tests-all') && featureKey && D?.modules) {
|
|
1175
1401
|
return D.modules
|
|
1176
1402
|
.filter(m => m.featureKeys?.includes(featureKey) && m.type !== 'test' && (!m.hasTests || m.testStale))
|
|
@@ -1254,36 +1480,36 @@ async function copyPromptForFile(featureKey, relPath, dropdown) {
|
|
|
1254
1480
|
}
|
|
1255
1481
|
|
|
1256
1482
|
// ─── Agent menu ─────────────────────────────────────────────────────────────
|
|
1257
|
-
const CLAUDE_MODELS = [
|
|
1258
|
-
{ id: 'claude-sonnet-4-6', checkId: 'amSonnet46Check' },
|
|
1259
|
-
{ id: 'claude-opus-4-5', checkId: 'amOpus45Check' },
|
|
1260
|
-
{ id: 'claude-haiku-3-5', checkId: 'amHaiku35Check' },
|
|
1261
|
-
];
|
|
1262
|
-
|
|
1263
|
-
function updateAgentRightsInfo() {
|
|
1264
|
-
const el = document.getElementById('headerAgentRights');
|
|
1265
|
-
if (!el || !D) return;
|
|
1266
|
-
const runtime = D.agentRuntime || {};
|
|
1267
|
-
const sandbox = runtime.codexSandboxMode || 'read-only';
|
|
1268
|
-
const approval = runtime.approvalPolicy || 'never';
|
|
1269
|
-
|
|
1270
|
-
if (D.agent === 'codex') {
|
|
1271
|
-
el.textContent = `🔐 Codex: ${sandbox}, approval: ${approval}`;
|
|
1272
|
-
el.title = 'Права/режим выполнения Codex';
|
|
1273
|
-
return;
|
|
1274
|
-
}
|
|
1275
|
-
if (D.agent === 'claude') {
|
|
1276
|
-
el.textContent = '🔐 Claude: managed by Claude CLI';
|
|
1277
|
-
el.title = 'Права определяются Claude CLI';
|
|
1278
|
-
return;
|
|
1279
|
-
}
|
|
1280
|
-
el.textContent = '🔐 Агент не выбран';
|
|
1281
|
-
el.title = 'Выбери агента в меню';
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
function updateAgentBtn() {
|
|
1285
|
-
const btn = document.getElementById('agentBtn');
|
|
1286
|
-
if (!D) return;
|
|
1483
|
+
const CLAUDE_MODELS = [
|
|
1484
|
+
{ id: 'claude-sonnet-4-6', checkId: 'amSonnet46Check' },
|
|
1485
|
+
{ id: 'claude-opus-4-5', checkId: 'amOpus45Check' },
|
|
1486
|
+
{ id: 'claude-haiku-3-5', checkId: 'amHaiku35Check' },
|
|
1487
|
+
];
|
|
1488
|
+
|
|
1489
|
+
function updateAgentRightsInfo() {
|
|
1490
|
+
const el = document.getElementById('headerAgentRights');
|
|
1491
|
+
if (!el || !D) return;
|
|
1492
|
+
const runtime = D.agentRuntime || {};
|
|
1493
|
+
const sandbox = runtime.codexSandboxMode || 'read-only';
|
|
1494
|
+
const approval = runtime.approvalPolicy || 'never';
|
|
1495
|
+
|
|
1496
|
+
if (D.agent === 'codex') {
|
|
1497
|
+
el.textContent = `🔐 Codex: ${sandbox}, approval: ${approval}`;
|
|
1498
|
+
el.title = 'Права/режим выполнения Codex';
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (D.agent === 'claude') {
|
|
1502
|
+
el.textContent = '🔐 Claude: managed by Claude CLI';
|
|
1503
|
+
el.title = 'Права определяются Claude CLI';
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
el.textContent = '🔐 Агент не выбран';
|
|
1507
|
+
el.title = 'Выбери агента в меню';
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function updateAgentBtn() {
|
|
1511
|
+
const btn = document.getElementById('agentBtn');
|
|
1512
|
+
if (!D) return;
|
|
1287
1513
|
const a = D.agent;
|
|
1288
1514
|
const m = D.model;
|
|
1289
1515
|
// Button label: agent + model shortname
|
|
@@ -1343,7 +1569,7 @@ async function reauthAgent() {
|
|
|
1343
1569
|
await fetch('/api/agent-reauth', { method: 'POST' });
|
|
1344
1570
|
}
|
|
1345
1571
|
|
|
1346
|
-
async function runAgentTask(task, featureKey, filePath) {
|
|
1572
|
+
async function runAgentTask(task, featureKey, filePath, selectedFilePaths) {
|
|
1347
1573
|
document.getElementById('agentPanel').classList.add('open');
|
|
1348
1574
|
document.getElementById('termBtn').classList.add('term-active');
|
|
1349
1575
|
if (!agentRunning) {
|
|
@@ -1352,7 +1578,12 @@ async function runAgentTask(task, featureKey, filePath) {
|
|
|
1352
1578
|
await fetch('/api/run-agent', {
|
|
1353
1579
|
method: 'POST',
|
|
1354
1580
|
headers: { 'Content-Type': 'application/json' },
|
|
1355
|
-
body: JSON.stringify({
|
|
1581
|
+
body: JSON.stringify({
|
|
1582
|
+
task,
|
|
1583
|
+
featureKey,
|
|
1584
|
+
filePath: filePath || undefined,
|
|
1585
|
+
selectedFilePaths: Array.isArray(selectedFilePaths) ? selectedFilePaths : undefined,
|
|
1586
|
+
}),
|
|
1356
1587
|
});
|
|
1357
1588
|
}
|
|
1358
1589
|
|
|
@@ -1425,28 +1656,40 @@ async function init() {
|
|
|
1425
1656
|
]);
|
|
1426
1657
|
D = await res.json();
|
|
1427
1658
|
|
|
1428
|
-
if (statusRes) {
|
|
1429
|
-
const status = await statusRes.json().catch(() => ({}));
|
|
1430
|
-
D.agentRunning = status.agentRunning ?? false;
|
|
1431
|
-
}
|
|
1432
|
-
updateAgentBtn();
|
|
1433
|
-
updateAgentRightsInfo();
|
|
1434
|
-
|
|
1435
|
-
document.getElementById('projectName').textContent = D.projectName;
|
|
1436
|
-
document.getElementById('scannedAt').textContent =
|
|
1437
|
-
new Date(D.scannedAt).toLocaleTimeString();
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1659
|
+
if (statusRes) {
|
|
1660
|
+
const status = await statusRes.json().catch(() => ({}));
|
|
1661
|
+
D.agentRunning = status.agentRunning ?? false;
|
|
1662
|
+
}
|
|
1663
|
+
updateAgentBtn();
|
|
1664
|
+
updateAgentRightsInfo();
|
|
1665
|
+
|
|
1666
|
+
document.getElementById('projectName').textContent = D.projectName;
|
|
1667
|
+
document.getElementById('scannedAt').textContent =
|
|
1668
|
+
new Date(D.scannedAt).toLocaleTimeString();
|
|
1669
|
+
|
|
1670
|
+
contextMode = getModeFromPath();
|
|
1671
|
+
view = D.hasConfig ? 'features' : 'files';
|
|
1672
|
+
applyHashRoute();
|
|
1441
1673
|
|
|
1442
1674
|
if (!D.hasConfig) {
|
|
1443
1675
|
document.querySelector('[data-view="features"]').classList.add('disabled');
|
|
1676
|
+
modeStore.qa.view = 'files';
|
|
1677
|
+
}
|
|
1678
|
+
modeStore.qa.view = D.hasConfig ? modeStore.qa.view : 'files';
|
|
1679
|
+
restoreModeState(contextMode);
|
|
1680
|
+
if (contextMode === 'observability') {
|
|
1681
|
+
view = 'files';
|
|
1682
|
+
drillFeatureKey = null;
|
|
1683
|
+
drillTestType = null;
|
|
1684
|
+
activePanelKey = null;
|
|
1444
1685
|
}
|
|
1686
|
+
setModeRoute(contextMode, true);
|
|
1687
|
+
document.getElementById('searchInput').value = searchQuery;
|
|
1445
1688
|
|
|
1446
1689
|
document.getElementById('loading').style.display = 'none';
|
|
1447
|
-
renderStats();
|
|
1448
|
-
renderSidebar();
|
|
1449
|
-
renderContent();
|
|
1690
|
+
renderStats();
|
|
1691
|
+
renderSidebar();
|
|
1692
|
+
renderContent();
|
|
1450
1693
|
} catch (err) {
|
|
1451
1694
|
document.getElementById('loading').textContent = '❌ Failed to load: ' + err.message;
|
|
1452
1695
|
}
|
|
@@ -1460,7 +1703,21 @@ function renderStats() {
|
|
|
1460
1703
|
const pct = src.length ? Math.round(tested / src.length * 100) : 0;
|
|
1461
1704
|
|
|
1462
1705
|
let items;
|
|
1463
|
-
if (
|
|
1706
|
+
if (contextMode === 'observability') {
|
|
1707
|
+
const serviceSources = src.filter(m => m.type === 'service' || m.type === 'util' || m.type === 'other');
|
|
1708
|
+
const errorSignal = Object.values(D.testErrors || {}).reduce((acc, v) => acc + (v?.failed || 0), 0);
|
|
1709
|
+
const totalBytes = src.reduce((acc, m) => acc + (m.size || 0), 0);
|
|
1710
|
+
const avgKb = src.length ? Math.round(totalBytes / src.length / 1024) : 0;
|
|
1711
|
+
const noiseRatio = src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0;
|
|
1712
|
+
const missingStructured = serviceSources.filter(m => !m.hasTests).length;
|
|
1713
|
+
items = [
|
|
1714
|
+
{ v: serviceSources.length, l: 'Log Sources' },
|
|
1715
|
+
{ v: avgKb + 'kb', l: 'Avg Volume/File' },
|
|
1716
|
+
{ v: noiseRatio + '%', l: 'Noise Ratio' },
|
|
1717
|
+
{ v: errorSignal, l: 'Error Signal', c: errorSignal ? '#f85149' : undefined },
|
|
1718
|
+
{ v: missingStructured, l: 'Missing Fields', c: missingStructured ? '#e3b341' : undefined },
|
|
1719
|
+
];
|
|
1720
|
+
} else if (D.hasConfig && D.features) {
|
|
1464
1721
|
const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
|
|
1465
1722
|
items = [
|
|
1466
1723
|
{ v: D.features.length, l: 'Features' },
|
|
@@ -1486,13 +1743,44 @@ function renderStats() {
|
|
|
1486
1743
|
).join('');
|
|
1487
1744
|
}
|
|
1488
1745
|
|
|
1746
|
+
function renderModeSwitch() {
|
|
1747
|
+
const root = document.getElementById('modeSwitch');
|
|
1748
|
+
const modes = [
|
|
1749
|
+
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
1750
|
+
{ key: 'observability', label: 'Observability', hint: 'Логи, шум, error signal' },
|
|
1751
|
+
];
|
|
1752
|
+
root.innerHTML = modes.map(m => `
|
|
1753
|
+
<button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
|
|
1754
|
+
<span>${m.label}</span>
|
|
1755
|
+
<span class="mode-switch-hint">${m.hint}</span>
|
|
1756
|
+
</button>
|
|
1757
|
+
`).join('');
|
|
1758
|
+
root.querySelectorAll('.mode-switch-btn').forEach(btn => {
|
|
1759
|
+
btn.onclick = () => switchMode(btn.dataset.mode);
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1489
1763
|
// ─── Sidebar ──────────────────────────────────────────────────────────────────
|
|
1490
1764
|
function renderSidebar() {
|
|
1765
|
+
renderModeSwitch();
|
|
1766
|
+
const tabs = document.getElementById('viewTabs');
|
|
1767
|
+
const extra = document.getElementById('sidebarExtra');
|
|
1768
|
+
|
|
1769
|
+
if (contextMode === 'observability') {
|
|
1770
|
+
tabs.style.display = 'none';
|
|
1771
|
+
extra.innerHTML = `
|
|
1772
|
+
<div class="sidebar-label">Observability focus</div>
|
|
1773
|
+
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
|
|
1774
|
+
Источники логов и качество сигналов для triage инцидентов.
|
|
1775
|
+
</div>`;
|
|
1776
|
+
return;
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
tabs.style.display = 'flex';
|
|
1491
1780
|
document.querySelectorAll('.view-tab').forEach(t =>
|
|
1492
1781
|
t.classList.toggle('active', t.dataset.view === view)
|
|
1493
1782
|
);
|
|
1494
1783
|
|
|
1495
|
-
const extra = document.getElementById('sidebarExtra');
|
|
1496
1784
|
if (view !== 'files') { extra.innerHTML = ''; return; }
|
|
1497
1785
|
|
|
1498
1786
|
const types = [...new Set(D.modules.map(m => m.type))].sort();
|
|
@@ -1528,6 +1816,11 @@ function renderSidebar() {
|
|
|
1528
1816
|
// ─── Content ──────────────────────────────────────────────────────────────────
|
|
1529
1817
|
function renderContent() {
|
|
1530
1818
|
const c = document.getElementById('content');
|
|
1819
|
+
if (contextMode === 'observability') {
|
|
1820
|
+
renderObservability(c);
|
|
1821
|
+
return;
|
|
1822
|
+
}
|
|
1823
|
+
|
|
1531
1824
|
if (view === 'features') {
|
|
1532
1825
|
if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
|
|
1533
1826
|
else if (drillFeatureKey) renderFeatureDetail(c);
|
|
@@ -1537,6 +1830,66 @@ function renderContent() {
|
|
|
1537
1830
|
}
|
|
1538
1831
|
}
|
|
1539
1832
|
|
|
1833
|
+
function renderQaOnboarding() {
|
|
1834
|
+
return `<div class="onboarding-block"><h3>QA Coverage: что это?</h3><p>Этот экран помогает найти пробелы в тестах: сначала проверь покрытие по фичам, затем открой критичные непокрытые файлы и запусти генерацию/фиксы тестов.</p></div>`;
|
|
1835
|
+
}
|
|
1836
|
+
|
|
1837
|
+
function renderObservability(c) {
|
|
1838
|
+
const src = D.modules.filter(m => m.type !== 'test');
|
|
1839
|
+
const sourceGroups = [
|
|
1840
|
+
{ label: 'Service', count: src.filter(m => m.type === 'service').length },
|
|
1841
|
+
{ label: 'Util', count: src.filter(m => m.type === 'util').length },
|
|
1842
|
+
{ label: 'Other', count: src.filter(m => m.type === 'other').length },
|
|
1843
|
+
].filter(x => x.count > 0);
|
|
1844
|
+
const errorSignal = Object.values(D.testErrors || {}).reduce((acc, v) => acc + (v?.failed || 0), 0);
|
|
1845
|
+
const missingStructured = src.filter(m => (m.type === 'service' || m.type === 'util') && !m.hasTests).slice(0, 8);
|
|
1846
|
+
const noisy = [...src]
|
|
1847
|
+
.sort((a, b) => (b.size || 0) - (a.size || 0))
|
|
1848
|
+
.slice(0, 5)
|
|
1849
|
+
.map(m => ({ name: m.name, rel: m.relativePath, kb: Math.round((m.size || 0) / 1024) }));
|
|
1850
|
+
|
|
1851
|
+
c.innerHTML = `
|
|
1852
|
+
<div class="onboarding-block">
|
|
1853
|
+
<h3>Observability: что это?</h3>
|
|
1854
|
+
<p>Экран для контроля signal-to-noise: откуда идут логи, где растёт объём, сколько шума и какие поля/сигналы нужно структурировать в первую очередь.</p>
|
|
1855
|
+
</div>
|
|
1856
|
+
|
|
1857
|
+
<div class="obs-grid">
|
|
1858
|
+
<div class="obs-card">
|
|
1859
|
+
<div class="obs-title">Источники логов</div>
|
|
1860
|
+
<div class="obs-list">
|
|
1861
|
+
${sourceGroups.map(g => `<div class="obs-list-item"><span>${g.label}</span><strong>${g.count}</strong></div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
|
|
1862
|
+
</div>
|
|
1863
|
+
</div>
|
|
1864
|
+
|
|
1865
|
+
<div class="obs-card">
|
|
1866
|
+
<div class="obs-title">Volume (top files)</div>
|
|
1867
|
+
<div class="obs-list">
|
|
1868
|
+
${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>'}
|
|
1869
|
+
</div>
|
|
1870
|
+
</div>
|
|
1871
|
+
|
|
1872
|
+
<div class="obs-card">
|
|
1873
|
+
<div class="obs-title">Noise ratio</div>
|
|
1874
|
+
<div class="obs-value">${src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0}%</div>
|
|
1875
|
+
<div class="obs-sub">Доля источников без тестового контроля (proxy для noisy логирования).</div>
|
|
1876
|
+
</div>
|
|
1877
|
+
|
|
1878
|
+
<div class="obs-card">
|
|
1879
|
+
<div class="obs-title">Error signal</div>
|
|
1880
|
+
<div class="obs-value" style="color:${errorSignal ? 'var(--red)' : 'var(--green)'}">${errorSignal}</div>
|
|
1881
|
+
<div class="obs-sub">Сумма упавших тестов из последнего прогона как индикатор нестабильности сигналов.</div>
|
|
1882
|
+
</div>
|
|
1883
|
+
|
|
1884
|
+
<div class="obs-card" style="grid-column:1 / -1">
|
|
1885
|
+
<div class="obs-title">Missing structured fields (candidates)</div>
|
|
1886
|
+
<div class="obs-list">
|
|
1887
|
+
${missingStructured.map(m => `<div class="obs-list-item"><span>${m.relativePath}</span><strong>needs schema</strong></div>`).join('') || '<div class="obs-sub">Явных кандидатов не найдено</div>'}
|
|
1888
|
+
</div>
|
|
1889
|
+
</div>
|
|
1890
|
+
</div>`;
|
|
1891
|
+
}
|
|
1892
|
+
|
|
1540
1893
|
function backToFeatureDetail() {
|
|
1541
1894
|
drillTestType = null;
|
|
1542
1895
|
activePanelKey = null;
|
|
@@ -1544,9 +1897,62 @@ function backToFeatureDetail() {
|
|
|
1544
1897
|
renderContent();
|
|
1545
1898
|
}
|
|
1546
1899
|
|
|
1547
|
-
function backToFeatures() {
|
|
1548
|
-
setFeatureDrill(null);
|
|
1549
|
-
}
|
|
1900
|
+
function backToFeatures() {
|
|
1901
|
+
setFeatureDrill(null);
|
|
1902
|
+
}
|
|
1903
|
+
|
|
1904
|
+
|
|
1905
|
+
function toPct(v) {
|
|
1906
|
+
return Math.round((v || 0) * 100) + '%';
|
|
1907
|
+
}
|
|
1908
|
+
|
|
1909
|
+
function renderObservabilityOverview(c) {
|
|
1910
|
+
const o = D.observability;
|
|
1911
|
+
if (!o) return '';
|
|
1912
|
+
const metrics = [
|
|
1913
|
+
['noise_ratio', toPct(o.metrics.noise_ratio)],
|
|
1914
|
+
['error_actionability', toPct(o.metrics.error_actionability)],
|
|
1915
|
+
['structured_completeness', toPct(o.metrics.structured_completeness)],
|
|
1916
|
+
['coverage_of_key_flows', toPct(o.metrics.coverage_of_key_flows)],
|
|
1917
|
+
];
|
|
1918
|
+
const noisy = (o.topNoisyPatterns || []).slice(0, 5).map(i =>
|
|
1919
|
+
`<div class="obs-row"><span class="obs-priority-${i.priority}">[${i.priority}]</span> ${escapeHtml(i.pattern)} · x${i.count} → <b>${i.recommendation}</b></div>`
|
|
1920
|
+
).join('') || '<div class="obs-row">Нет шумных паттернов</div>';
|
|
1921
|
+
|
|
1922
|
+
const missing = (o.missingCriticalLogs || []).slice(0, 5).map(i =>
|
|
1923
|
+
`<div class="obs-row"><span class="obs-priority-${i.priority}">[${i.priority}]</span> ${escapeHtml(i.pattern)} → <b>${i.recommendation}</b></div>`
|
|
1924
|
+
).join('') || '<div class="obs-row">Критичные логи покрыты</div>';
|
|
1925
|
+
|
|
1926
|
+
const catalogRows = (o.catalog || []).slice(0, 10).map(i =>
|
|
1927
|
+
`<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>`
|
|
1928
|
+
).join('');
|
|
1929
|
+
|
|
1930
|
+
return `
|
|
1931
|
+
<div class="observability-panel">
|
|
1932
|
+
<div class="observability-title">Observability</div>
|
|
1933
|
+
<div class="obs-metrics">
|
|
1934
|
+
${metrics.map(([l,v]) => `<div class="obs-metric"><div class="obs-metric-v">${v}</div><div class="obs-metric-l">${l}</div></div>`).join('')}
|
|
1935
|
+
</div>
|
|
1936
|
+
<div class="obs-columns">
|
|
1937
|
+
<div class="obs-list">
|
|
1938
|
+
<h4>Top noisy patterns</h4>
|
|
1939
|
+
${noisy}
|
|
1940
|
+
</div>
|
|
1941
|
+
<div class="obs-list">
|
|
1942
|
+
<h4>Missing critical logs</h4>
|
|
1943
|
+
${missing}
|
|
1944
|
+
</div>
|
|
1945
|
+
</div>
|
|
1946
|
+
<div class="obs-catalog">
|
|
1947
|
+
<h4>Каталог источников логов (top 10)</h4>
|
|
1948
|
+
<div class="obs-cat-row head"><span>module</span><span>level</span><span>format</span><span>frequency</span><span>owner</span><span>action</span></div>
|
|
1949
|
+
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
1950
|
+
</div>
|
|
1951
|
+
<div class="obs-row" style="margin-top:8px">
|
|
1952
|
+
Классификация: мусор ${o.classification.trash} · полезно ${o.classification.useful} · критично ${o.classification.critical}
|
|
1953
|
+
</div>
|
|
1954
|
+
</div>`;
|
|
1955
|
+
}
|
|
1550
1956
|
|
|
1551
1957
|
function renderFeatureCards(c) {
|
|
1552
1958
|
if (!D.hasConfig || !D.features) {
|
|
@@ -1579,7 +1985,7 @@ function renderFeatureCards(c) {
|
|
|
1579
1985
|
</div>
|
|
1580
1986
|
</div>` : '';
|
|
1581
1987
|
|
|
1582
|
-
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
1988
|
+
c.innerHTML = setupBanner + renderObservabilityOverview(c) + '<div class="features-grid" id="featGrid"></div>';
|
|
1583
1989
|
const grid = document.getElementById('featGrid');
|
|
1584
1990
|
|
|
1585
1991
|
list.forEach(f => {
|
|
@@ -1622,22 +2028,22 @@ function renderFeatureCards(c) {
|
|
|
1622
2028
|
▶ Написать тесты (${f.fileCount - f.testedCount} без тестов)
|
|
1623
2029
|
</button>` : ''}
|
|
1624
2030
|
</div>`;
|
|
1625
|
-
card.onmousedown = (e) => {
|
|
1626
|
-
if (e.button === 1 && !e.target.closest('.agent-card-btn')) e.preventDefault();
|
|
1627
|
-
};
|
|
1628
|
-
card.onauxclick = (e) => {
|
|
1629
|
-
if (e.button !== 1 || e.target.closest('.agent-card-btn')) return;
|
|
1630
|
-
e.preventDefault();
|
|
1631
|
-
openFeatureInNewTab(f.key);
|
|
1632
|
-
};
|
|
1633
|
-
card.onclick = (e) => {
|
|
1634
|
-
if (e.target.closest('.agent-card-btn')) return; // don't drill on agent btn click
|
|
1635
|
-
if (e.metaKey || e.ctrlKey) {
|
|
1636
|
-
openFeatureInNewTab(f.key);
|
|
1637
|
-
return;
|
|
1638
|
-
}
|
|
1639
|
-
setFeatureDrill(f.key);
|
|
1640
|
-
};
|
|
2031
|
+
card.onmousedown = (e) => {
|
|
2032
|
+
if (e.button === 1 && !e.target.closest('.agent-card-btn')) e.preventDefault();
|
|
2033
|
+
};
|
|
2034
|
+
card.onauxclick = (e) => {
|
|
2035
|
+
if (e.button !== 1 || e.target.closest('.agent-card-btn')) return;
|
|
2036
|
+
e.preventDefault();
|
|
2037
|
+
openFeatureInNewTab(f.key);
|
|
2038
|
+
};
|
|
2039
|
+
card.onclick = (e) => {
|
|
2040
|
+
if (e.target.closest('.agent-card-btn')) return; // don't drill on agent btn click
|
|
2041
|
+
if (e.metaKey || e.ctrlKey) {
|
|
2042
|
+
openFeatureInNewTab(f.key);
|
|
2043
|
+
return;
|
|
2044
|
+
}
|
|
2045
|
+
setFeatureDrill(f.key);
|
|
2046
|
+
};
|
|
1641
2047
|
const agentBtn = card.querySelector('.agent-card-btn');
|
|
1642
2048
|
if (agentBtn) {
|
|
1643
2049
|
agentBtn.onclick = (e) => {
|
|
@@ -1678,21 +2084,21 @@ function renderFeatureCards(c) {
|
|
|
1678
2084
|
<span class="feature-progress-label" style="color:var(--dim)">нет привязки</span>
|
|
1679
2085
|
</div>
|
|
1680
2086
|
</div>`;
|
|
1681
|
-
card.onmousedown = (e) => {
|
|
1682
|
-
if (e.button === 1) e.preventDefault();
|
|
1683
|
-
};
|
|
1684
|
-
card.onauxclick = (e) => {
|
|
1685
|
-
if (e.button !== 1) return;
|
|
1686
|
-
e.preventDefault();
|
|
1687
|
-
openFeatureInNewTab('__unmapped__');
|
|
1688
|
-
};
|
|
1689
|
-
card.onclick = (e) => {
|
|
1690
|
-
if (e.metaKey || e.ctrlKey) {
|
|
1691
|
-
openFeatureInNewTab('__unmapped__');
|
|
1692
|
-
return;
|
|
1693
|
-
}
|
|
1694
|
-
setFeatureDrill('__unmapped__');
|
|
1695
|
-
};
|
|
2087
|
+
card.onmousedown = (e) => {
|
|
2088
|
+
if (e.button === 1) e.preventDefault();
|
|
2089
|
+
};
|
|
2090
|
+
card.onauxclick = (e) => {
|
|
2091
|
+
if (e.button !== 1) return;
|
|
2092
|
+
e.preventDefault();
|
|
2093
|
+
openFeatureInNewTab('__unmapped__');
|
|
2094
|
+
};
|
|
2095
|
+
card.onclick = (e) => {
|
|
2096
|
+
if (e.metaKey || e.ctrlKey) {
|
|
2097
|
+
openFeatureInNewTab('__unmapped__');
|
|
2098
|
+
return;
|
|
2099
|
+
}
|
|
2100
|
+
setFeatureDrill('__unmapped__');
|
|
2101
|
+
};
|
|
1696
2102
|
grid.appendChild(card);
|
|
1697
2103
|
}
|
|
1698
2104
|
}
|
|
@@ -1884,11 +2290,12 @@ function renderFeatureDetail(c) {
|
|
|
1884
2290
|
return;
|
|
1885
2291
|
}
|
|
1886
2292
|
|
|
1887
|
-
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
1888
|
-
const src = mods.filter(m => m.type !== 'test');
|
|
1889
|
-
const
|
|
1890
|
-
const
|
|
1891
|
-
const
|
|
2293
|
+
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
2294
|
+
const src = mods.filter(m => m.type !== 'test');
|
|
2295
|
+
const untestedSrc = src.filter(m => !m.hasTests);
|
|
2296
|
+
const tst = mods.filter(m => m.type === 'test');
|
|
2297
|
+
const testedCount = src.filter(m => m.hasTests).length;
|
|
2298
|
+
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
1892
2299
|
|
|
1893
2300
|
const unitCount = feat.unitTestCount ?? tst.filter(m => m.testType === 'unit').length;
|
|
1894
2301
|
const integrationCount = feat.integrationTestCount ?? tst.filter(m => m.testType === 'integration').length;
|
|
@@ -1900,20 +2307,28 @@ function renderFeatureDetail(c) {
|
|
|
1900
2307
|
const integrationFailed = tst.filter(m => m.testType === 'integration' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
1901
2308
|
const e2eFailed = tst.filter(m => m.testType === 'e2e' && te[m.relativePath.replace(/\\/g, '/')]).length;
|
|
1902
2309
|
|
|
1903
|
-
// Determine what list to show based on active tab
|
|
1904
|
-
// null or 'source' → source files; test type → test files of that type
|
|
1905
|
-
const activeTab = drillTestType || 'source';
|
|
1906
|
-
const
|
|
1907
|
-
const
|
|
1908
|
-
const
|
|
1909
|
-
const
|
|
1910
|
-
|
|
1911
|
-
|
|
2310
|
+
// Determine what list to show based on active tab
|
|
2311
|
+
// null or 'source' → source files; test type → test files of that type
|
|
2312
|
+
const activeTab = drillTestType || 'source';
|
|
2313
|
+
const sourceList = showOnlyUntestedInFeature ? untestedSrc : src;
|
|
2314
|
+
const listFiles = activeTab === 'source' ? sourceList : tst.filter(m => m.testType === activeTab);
|
|
2315
|
+
const isTestList = activeTab !== 'source';
|
|
2316
|
+
const showUntestedToggle = activeTab === 'source';
|
|
2317
|
+
const meta = TEST_TYPE_META[activeTab];
|
|
2318
|
+
const listLabel = meta
|
|
2319
|
+
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
2320
|
+
: `📁 Файлы фичи (${listFiles.length})`;
|
|
1912
2321
|
|
|
1913
2322
|
const q = searchQuery.toLowerCase();
|
|
1914
2323
|
const filtered = q ? listFiles.filter(m =>
|
|
1915
2324
|
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
1916
2325
|
) : listFiles;
|
|
2326
|
+
const selectableSource = !isTestList && !!D.agent;
|
|
2327
|
+
const visibleSourcePaths = selectableSource
|
|
2328
|
+
? filtered.map(m => m.relativePath.replace(/\\/g, '/'))
|
|
2329
|
+
: [];
|
|
2330
|
+
const selectedVisible = selectableSource ? selectedFilesForFeature(drillFeatureKey, filtered.map(m => m.relativePath)) : [];
|
|
2331
|
+
const selectedCount = selectedVisible.length;
|
|
1917
2332
|
|
|
1918
2333
|
c.innerHTML = `
|
|
1919
2334
|
<div class="drill-header">
|
|
@@ -1939,16 +2354,81 @@ function renderFeatureDetail(c) {
|
|
|
1939
2354
|
${testTypeCard('integration', 'Integration', '🔗', '#58a6ff', integrationCount, activeTab === 'integration', drillFeatureKey, integrationFailed)}
|
|
1940
2355
|
${testTypeCard('e2e', 'E2E', '🎭', '#d2a8ff', e2eCount, activeTab === 'e2e', drillFeatureKey, e2eFailed)}
|
|
1941
2356
|
</div>
|
|
1942
|
-
|
|
1943
|
-
<div class="drill-section-label">${listLabel}</div>
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
2357
|
+
|
|
2358
|
+
<div class="drill-section-label">${listLabel}</div>
|
|
2359
|
+
${showUntestedToggle ? `
|
|
2360
|
+
<div class="feature-file-filters">
|
|
2361
|
+
<label class="feature-filter-toggle">
|
|
2362
|
+
<input type="checkbox" id="untestedOnlyToggle" ${showOnlyUntestedInFeature ? 'checked' : ''}>
|
|
2363
|
+
<span>Только без тестов</span>
|
|
2364
|
+
<span class="feature-filter-meta">(${untestedSrc.length})</span>
|
|
2365
|
+
</label>
|
|
2366
|
+
</div>` : ''}
|
|
2367
|
+
${selectableSource ? `
|
|
2368
|
+
<div class="bulk-actions">
|
|
2369
|
+
<span class="bulk-actions-count">Выбрано файлов: <b>${selectedCount}</b></span>
|
|
2370
|
+
<button class="bulk-actions-btn" id="bulkSelectAllBtn" ${visibleSourcePaths.length === 0 ? 'disabled' : ''}>☑ Выбрать все (${visibleSourcePaths.length})</button>
|
|
2371
|
+
<button class="bulk-actions-btn" id="bulkClearBtn" ${selectedCount === 0 ? 'disabled' : ''}>Снять выбор</button>
|
|
2372
|
+
<button class="bulk-actions-btn primary" id="bulkWriteTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>✍ Написать тесты для выбранных</button>
|
|
2373
|
+
<button class="bulk-actions-btn" id="bulkRefreshTestsBtn" ${selectedCount === 0 ? 'disabled' : ''}>↻ Актуализировать тесты</button>
|
|
2374
|
+
</div>` : ''}
|
|
2375
|
+
<div class="file-rows" id="fileRows">
|
|
2376
|
+
${filtered.length === 0
|
|
2377
|
+
? `<div style="padding:20px;text-align:center;border:1px dashed var(--border);border-radius:8px;color:var(--dim);font-size:13px">
|
|
2378
|
+
${isTestList
|
|
2379
|
+
? 'Нет тестов этого типа для данной фичи'
|
|
2380
|
+
: (showOnlyUntestedInFeature
|
|
2381
|
+
? 'Все файлы этой фичи уже покрыты тестами'
|
|
2382
|
+
: 'Нет файлов — возможно паттерны в конфиге не совпадают')}
|
|
2383
|
+
</div>`
|
|
2384
|
+
: filtered.map(m => fileRow(m, isTestList, drillFeatureKey, selectableSource)).join('')
|
|
2385
|
+
}
|
|
2386
|
+
</div>`;
|
|
2387
|
+
|
|
2388
|
+
const untestedOnlyToggle = document.getElementById('untestedOnlyToggle');
|
|
2389
|
+
if (untestedOnlyToggle) {
|
|
2390
|
+
untestedOnlyToggle.onchange = (e) => {
|
|
2391
|
+
showOnlyUntestedInFeature = !!e.target.checked;
|
|
2392
|
+
renderContent();
|
|
2393
|
+
};
|
|
2394
|
+
}
|
|
2395
|
+
|
|
2396
|
+
if (selectableSource) {
|
|
2397
|
+
const allVisibleSelected = visibleSourcePaths.length > 0 && visibleSourcePaths.every((p) => selectedSourceFiles.has(p));
|
|
2398
|
+
const bulkSelectAllBtn = document.getElementById('bulkSelectAllBtn');
|
|
2399
|
+
if (bulkSelectAllBtn) {
|
|
2400
|
+
bulkSelectAllBtn.textContent = allVisibleSelected
|
|
2401
|
+
? `☑ Выбраны все (${visibleSourcePaths.length})`
|
|
2402
|
+
: `☑ Выбрать все (${visibleSourcePaths.length})`;
|
|
2403
|
+
bulkSelectAllBtn.onclick = () => {
|
|
2404
|
+
visibleSourcePaths.forEach((p) => selectedSourceFiles.add(p));
|
|
2405
|
+
renderContent();
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
const bulkClearBtn = document.getElementById('bulkClearBtn');
|
|
2409
|
+
if (bulkClearBtn) {
|
|
2410
|
+
bulkClearBtn.onclick = () => {
|
|
2411
|
+
selectedVisible.forEach((p) => selectedSourceFiles.delete(p));
|
|
2412
|
+
renderContent();
|
|
2413
|
+
};
|
|
2414
|
+
}
|
|
2415
|
+
const bulkWriteTestsBtn = document.getElementById('bulkWriteTestsBtn');
|
|
2416
|
+
if (bulkWriteTestsBtn) {
|
|
2417
|
+
bulkWriteTestsBtn.onclick = () => {
|
|
2418
|
+
runAgentTask('write-tests-selected', drillFeatureKey, null, selectedVisible);
|
|
2419
|
+
selectedVisible.forEach((p) => selectedSourceFiles.delete(p));
|
|
2420
|
+
renderContent();
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
const bulkRefreshTestsBtn = document.getElementById('bulkRefreshTestsBtn');
|
|
2424
|
+
if (bulkRefreshTestsBtn) {
|
|
2425
|
+
bulkRefreshTestsBtn.onclick = () => {
|
|
2426
|
+
runAgentTask('refresh-tests-selected', drillFeatureKey, null, selectedVisible);
|
|
2427
|
+
selectedVisible.forEach((p) => selectedSourceFiles.delete(p));
|
|
2428
|
+
renderContent();
|
|
2429
|
+
};
|
|
2430
|
+
}
|
|
2431
|
+
}
|
|
1952
2432
|
|
|
1953
2433
|
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
1954
2434
|
card.onclick = async () => {
|
|
@@ -2106,7 +2586,7 @@ function renderUnmappedDetail(c) {
|
|
|
2106
2586
|
});
|
|
2107
2587
|
}
|
|
2108
2588
|
|
|
2109
|
-
function fileRow(m, isTest = false, featureKey = null) {
|
|
2589
|
+
function fileRow(m, isTest = false, featureKey = null, selectable = false) {
|
|
2110
2590
|
const relPath = m.relativePath.replace(/\\/g, '/');
|
|
2111
2591
|
const parts = relPath.split('/');
|
|
2112
2592
|
const name = parts[parts.length - 1];
|
|
@@ -2119,6 +2599,7 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
2119
2599
|
? `<span class="file-agent-spinner ${agentState}" title="${agentState === 'running' ? 'Агент работает…' : 'В очереди…'}"></span>`
|
|
2120
2600
|
: (testErr ? '❌' : isTest ? '🧪' : (m.testStale ? '⚠️' : m.hasTests ? '✅' : '⬜'));
|
|
2121
2601
|
const isActive = activePanelKey === m.id;
|
|
2602
|
+
const isSelected = selectedSourceFiles.has(relPath);
|
|
2122
2603
|
|
|
2123
2604
|
// Write-test button for source files
|
|
2124
2605
|
// In feature drill-down (featureKey set), show button even for isInfra files — user explicitly added them to the feature
|
|
@@ -2167,8 +2648,14 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
2167
2648
|
</div>`
|
|
2168
2649
|
: '';
|
|
2169
2650
|
|
|
2651
|
+
const selectBox = selectable
|
|
2652
|
+
? `<input type="checkbox" class="file-row-select" ${isSelected ? 'checked' : ''}
|
|
2653
|
+
onclick="event.stopPropagation();toggleSourceSelection('${relPath}', this.checked)">`
|
|
2654
|
+
: '';
|
|
2655
|
+
|
|
2170
2656
|
return `
|
|
2171
2657
|
<div class="file-row${isActive ? ' active' : ''}${testErr ? ' has-errors' : ''}" data-id="${m.id}">
|
|
2658
|
+
${selectBox}
|
|
2172
2659
|
<span class="file-row-icon">${icon}</span>
|
|
2173
2660
|
<span class="file-row-name">${name}</span>
|
|
2174
2661
|
${errBadge}
|
|
@@ -2180,6 +2667,13 @@ function fileRow(m, isTest = false, featureKey = null) {
|
|
|
2180
2667
|
${errHtml}`;
|
|
2181
2668
|
}
|
|
2182
2669
|
|
|
2670
|
+
function toggleSourceSelection(relPath, checked) {
|
|
2671
|
+
const n = relPath.replace(/\\/g, '/');
|
|
2672
|
+
if (checked) selectedSourceFiles.add(n);
|
|
2673
|
+
else selectedSourceFiles.delete(n);
|
|
2674
|
+
renderContent();
|
|
2675
|
+
}
|
|
2676
|
+
|
|
2183
2677
|
function renderModuleGrid(c) {
|
|
2184
2678
|
const q = searchQuery.toLowerCase();
|
|
2185
2679
|
const list = D.modules.filter(m => {
|
|
@@ -2429,36 +2923,54 @@ function closePanel() {
|
|
|
2429
2923
|
}
|
|
2430
2924
|
|
|
2431
2925
|
// ─── Events ───────────────────────────────────────────────────────────────────
|
|
2432
|
-
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
2433
|
-
tab.onclick = () => {
|
|
2434
|
-
if (
|
|
2435
|
-
|
|
2436
|
-
|
|
2926
|
+
document.querySelectorAll('.view-tab').forEach(tab => {
|
|
2927
|
+
tab.onclick = () => {
|
|
2928
|
+
if (contextMode !== 'qa') return;
|
|
2929
|
+
if (tab.classList.contains('disabled')) return;
|
|
2930
|
+
view = tab.dataset.view;
|
|
2931
|
+
drillFeatureKey = null;
|
|
2437
2932
|
drillTestType = null;
|
|
2933
|
+
selectedSourceFiles.clear();
|
|
2438
2934
|
activePanelKey = null;
|
|
2439
2935
|
searchQuery = '';
|
|
2440
|
-
activeTypes.clear();
|
|
2441
|
-
document.getElementById('searchInput').value = '';
|
|
2442
|
-
document.getElementById('panel').classList.remove('open');
|
|
2443
|
-
if (view !== 'features') clearFeatureHash();
|
|
2444
|
-
renderSidebar();
|
|
2445
|
-
renderContent();
|
|
2446
|
-
};
|
|
2447
|
-
});
|
|
2936
|
+
activeTypes.clear();
|
|
2937
|
+
document.getElementById('searchInput').value = '';
|
|
2938
|
+
document.getElementById('panel').classList.remove('open');
|
|
2939
|
+
if (view !== 'features') clearFeatureHash();
|
|
2940
|
+
renderSidebar();
|
|
2941
|
+
renderContent();
|
|
2942
|
+
};
|
|
2943
|
+
});
|
|
2448
2944
|
|
|
2449
2945
|
document.getElementById('searchInput').oninput = e => {
|
|
2450
2946
|
searchQuery = e.target.value.toLowerCase();
|
|
2451
2947
|
renderContent();
|
|
2452
2948
|
};
|
|
2453
2949
|
|
|
2454
|
-
document.getElementById('panelClose').onclick = closePanel;
|
|
2455
|
-
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); });
|
|
2456
|
-
window.addEventListener('hashchange', () => {
|
|
2457
|
-
if (!D) return;
|
|
2458
|
-
applyHashRoute();
|
|
2459
|
-
renderSidebar();
|
|
2460
|
-
renderContent();
|
|
2461
|
-
});
|
|
2950
|
+
document.getElementById('panelClose').onclick = closePanel;
|
|
2951
|
+
document.addEventListener('keydown', e => { if (e.key === 'Escape') closePanel(); });
|
|
2952
|
+
window.addEventListener('hashchange', () => {
|
|
2953
|
+
if (!D || contextMode !== 'qa') return;
|
|
2954
|
+
applyHashRoute();
|
|
2955
|
+
renderSidebar();
|
|
2956
|
+
renderContent();
|
|
2957
|
+
});
|
|
2958
|
+
|
|
2959
|
+
|
|
2960
|
+
window.addEventListener('popstate', () => {
|
|
2961
|
+
if (!D) return;
|
|
2962
|
+
const routeMode = getModeFromPath();
|
|
2963
|
+
if (routeMode !== contextMode) {
|
|
2964
|
+
saveModeState(contextMode);
|
|
2965
|
+
contextMode = routeMode;
|
|
2966
|
+
restoreModeState(contextMode);
|
|
2967
|
+
}
|
|
2968
|
+
if (contextMode === 'qa') applyHashRoute();
|
|
2969
|
+
document.getElementById('searchInput').value = searchQuery;
|
|
2970
|
+
renderStats();
|
|
2971
|
+
renderSidebar();
|
|
2972
|
+
renderContent();
|
|
2973
|
+
});
|
|
2462
2974
|
|
|
2463
2975
|
// ─── Live reload ──────────────────────────────────────────────────────────────
|
|
2464
2976
|
function setLiveDot(color, title) {
|
|
@@ -2476,11 +2988,11 @@ async function refreshData() {
|
|
|
2476
2988
|
document.getElementById('scannedAt').textContent =
|
|
2477
2989
|
new Date(D.scannedAt).toLocaleTimeString();
|
|
2478
2990
|
|
|
2479
|
-
renderStats();
|
|
2480
|
-
renderSidebar();
|
|
2481
|
-
renderContent();
|
|
2482
|
-
updateAgentBtn();
|
|
2483
|
-
updateAgentRightsInfo();
|
|
2991
|
+
renderStats();
|
|
2992
|
+
renderSidebar();
|
|
2993
|
+
renderContent();
|
|
2994
|
+
updateAgentBtn();
|
|
2995
|
+
updateAgentRightsInfo();
|
|
2484
2996
|
|
|
2485
2997
|
// Re-render drill-down or re-open panel
|
|
2486
2998
|
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
@@ -2515,12 +3027,12 @@ function connectSSE() {
|
|
|
2515
3027
|
});
|
|
2516
3028
|
|
|
2517
3029
|
es.addEventListener('agent-queued', (e) => {
|
|
2518
|
-
const { queueLength, title, task, featureKey, filePath } = JSON.parse(e.data);
|
|
3030
|
+
const { queueLength, title, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
2519
3031
|
updateQueueBadge(queueLength);
|
|
2520
3032
|
document.getElementById('agentPanel').classList.add('open');
|
|
2521
3033
|
document.getElementById('termBtn').classList.add('term-active');
|
|
2522
3034
|
// Track queued paths for spinner
|
|
2523
|
-
getTaskFilePaths(task, featureKey, filePath).forEach(p => agentQueuedPaths.add(p));
|
|
3035
|
+
getTaskFilePaths(task, featureKey, filePath, selectedFilePaths).forEach(p => agentQueuedPaths.add(p));
|
|
2524
3036
|
renderContent();
|
|
2525
3037
|
// Append queue notification to the currently running session (or active)
|
|
2526
3038
|
const targetId = runningSessionId || activeSessionId;
|
|
@@ -2531,10 +3043,10 @@ function connectSSE() {
|
|
|
2531
3043
|
|
|
2532
3044
|
es.addEventListener('agent-started', (e) => {
|
|
2533
3045
|
setAgentRunning(true);
|
|
2534
|
-
const { title, queueLength = 0, task, featureKey, filePath } = JSON.parse(e.data);
|
|
3046
|
+
const { title, queueLength = 0, task, featureKey, filePath, selectedFilePaths } = JSON.parse(e.data);
|
|
2535
3047
|
updateQueueBadge(queueLength);
|
|
2536
3048
|
// Move paths from queued → running (current task)
|
|
2537
|
-
const startedPaths = getTaskFilePaths(task, featureKey, filePath);
|
|
3049
|
+
const startedPaths = getTaskFilePaths(task, featureKey, filePath, selectedFilePaths);
|
|
2538
3050
|
startedPaths.forEach(p => { agentQueuedPaths.delete(p); agentRunningPaths.add(p); });
|
|
2539
3051
|
renderContent();
|
|
2540
3052
|
// Close previous session (queue case: agent-done not fired between tasks)
|
|
@@ -2575,37 +3087,37 @@ function connectSSE() {
|
|
|
2575
3087
|
renderContent();
|
|
2576
3088
|
});
|
|
2577
3089
|
|
|
2578
|
-
es.addEventListener('agent-summary', (e) => {
|
|
2579
|
-
const {
|
|
2580
|
-
passed, failed, files = [],
|
|
2581
|
-
testedFileCount = files.length,
|
|
2582
|
-
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
2583
|
-
failedFileCount = failed > 0 ? 1 : 0,
|
|
2584
|
-
autoFixQueued = false,
|
|
2585
|
-
} = JSON.parse(e.data);
|
|
2586
|
-
const allOk = failed === 0;
|
|
2587
|
-
const box = document.createElement('div');
|
|
2588
|
-
box.style.cssText = `
|
|
2589
|
-
margin: 10px 0 4px;
|
|
2590
|
-
padding: 10px 14px;
|
|
3090
|
+
es.addEventListener('agent-summary', (e) => {
|
|
3091
|
+
const {
|
|
3092
|
+
passed, failed, files = [],
|
|
3093
|
+
testedFileCount = files.length,
|
|
3094
|
+
passedFileCount = Math.max(0, testedFileCount - (failed > 0 ? 1 : 0)),
|
|
3095
|
+
failedFileCount = failed > 0 ? 1 : 0,
|
|
3096
|
+
autoFixQueued = false,
|
|
3097
|
+
} = JSON.parse(e.data);
|
|
3098
|
+
const allOk = failed === 0;
|
|
3099
|
+
const box = document.createElement('div');
|
|
3100
|
+
box.style.cssText = `
|
|
3101
|
+
margin: 10px 0 4px;
|
|
3102
|
+
padding: 10px 14px;
|
|
2591
3103
|
border-radius: 8px;
|
|
2592
3104
|
border: 1px solid ${allOk ? 'var(--green)' : 'var(--red)'};
|
|
2593
3105
|
background: ${allOk ? '#0d2a1a' : '#2a0d0d'};
|
|
2594
3106
|
font-family: inherit;
|
|
2595
|
-
`;
|
|
2596
|
-
box.innerHTML = `
|
|
2597
|
-
<div style="font-size:13px;font-weight:700;color:${allOk ? 'var(--green)' : 'var(--red)'}">
|
|
2598
|
-
${allOk ? '✅' : '⚠️'} Тесты: ${allOk ? 'всё ок' : 'есть падения'}
|
|
2599
|
-
</div>
|
|
2600
|
-
<div style="font-size:11px;color:var(--muted);margin-top:4px">
|
|
2601
|
-
Файлы: ${testedFileCount} • passed: ${passedFileCount} • failed: ${failedFileCount}
|
|
2602
|
-
</div>
|
|
2603
|
-
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
2604
|
-
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
2605
|
-
</div>
|
|
2606
|
-
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
2607
|
-
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
2608
|
-
`;
|
|
3107
|
+
`;
|
|
3108
|
+
box.innerHTML = `
|
|
3109
|
+
<div style="font-size:13px;font-weight:700;color:${allOk ? 'var(--green)' : 'var(--red)'}">
|
|
3110
|
+
${allOk ? '✅' : '⚠️'} Тесты: ${allOk ? 'всё ок' : 'есть падения'}
|
|
3111
|
+
</div>
|
|
3112
|
+
<div style="font-size:11px;color:var(--muted);margin-top:4px">
|
|
3113
|
+
Файлы: ${testedFileCount} • passed: ${passedFileCount} • failed: ${failedFileCount}
|
|
3114
|
+
</div>
|
|
3115
|
+
<div style="font-size:11px;color:var(--muted);margin-top:2px">
|
|
3116
|
+
Тест-кейсы: ${passed} passed${failed > 0 ? ` • ${failed} failed` : ''}
|
|
3117
|
+
</div>
|
|
3118
|
+
${autoFixQueued ? `<div style="font-size:11px;color:var(--yellow);margin-top:4px">🛠️ Автоисправление поставлено в очередь</div>` : ''}
|
|
3119
|
+
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
3120
|
+
`;
|
|
2609
3121
|
const targetId = runningSessionId || activeSessionId;
|
|
2610
3122
|
if (targetId) {
|
|
2611
3123
|
appendToSession(targetId, box);
|