viberadar 0.3.72 → 0.3.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/scanner/index.d.ts +18 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +265 -9
- package/dist/scanner/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +465 -16
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +408 -52
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
--green: #3fb950;
|
|
20
20
|
--red: #f85149;
|
|
21
21
|
--yellow: #e3b341;
|
|
22
|
+
--accent: #58a6ff;
|
|
22
23
|
}
|
|
23
24
|
|
|
24
25
|
body {
|
|
@@ -45,6 +46,15 @@
|
|
|
45
46
|
header h1 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; }
|
|
46
47
|
.header-project { margin-left: auto; font-size: 13px; color: var(--muted); }
|
|
47
48
|
.header-time { font-size: 12px; color: var(--dim); }
|
|
49
|
+
#rescanBtn { font-size: 16px; padding: 2px 6px; background: none; border: none; cursor: pointer; opacity: 0.7; transition: opacity 0.2s; }
|
|
50
|
+
#rescanBtn:hover { opacity: 1; }
|
|
51
|
+
#rescanBtn.spinning { animation: rescan-spin 0.8s linear infinite; opacity: 1; }
|
|
52
|
+
@keyframes rescan-spin { to { transform: rotate(360deg); } }
|
|
53
|
+
#rescanStatus { font-size: 11px; padding: 2px 8px; border-radius: 999px; border: 1px solid transparent; transition: all 0.4s; white-space: nowrap; }
|
|
54
|
+
#rescanStatus.status-idle { color: var(--dim); border-color: transparent; }
|
|
55
|
+
#rescanStatus.status-scanning { color: var(--yellow); border-color: rgba(227,179,65,0.3); }
|
|
56
|
+
#rescanStatus.status-done { color: var(--green); border-color: rgba(63,185,80,0.35); background: rgba(63,185,80,0.07); }
|
|
57
|
+
#rescanStatus.status-error { color: var(--red); border-color: rgba(248,81,73,0.3); }
|
|
48
58
|
.header-agent-rights {
|
|
49
59
|
font-size: 11px;
|
|
50
60
|
color: var(--muted);
|
|
@@ -191,7 +201,7 @@
|
|
|
191
201
|
.obs-priority-low { color: var(--green); }
|
|
192
202
|
.obs-catalog { margin-top:10px; background: var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px; }
|
|
193
203
|
.obs-catalog h4 { font-size:12px; margin-bottom:6px; }
|
|
194
|
-
.obs-cat-row { display:grid; grid-template-columns: 1.
|
|
204
|
+
.obs-cat-row { display:grid; grid-template-columns: 1.4fr .5fr .6fr .4fr 1.2fr .7fr auto; gap:6px; font-size:11px; color:var(--muted); padding:5px 0; border-bottom:1px dashed var(--border); }
|
|
195
205
|
.obs-cat-row.head { color: var(--text); font-weight:600; text-transform:uppercase; font-size:10px; }
|
|
196
206
|
|
|
197
207
|
/* ── Layout ──────────────────────────────────────────────────────────────── */
|
|
@@ -651,14 +661,48 @@
|
|
|
651
661
|
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
652
662
|
}
|
|
653
663
|
.file-row:hover .file-row-agent-btn { border-color: var(--accent); color: var(--accent); }
|
|
654
|
-
.file-row-agent-btn:hover { background: var(--accent); color:
|
|
664
|
+
.file-row-agent-btn:hover { background: var(--accent); color: var(--bg) !important; border-color: var(--accent); }
|
|
655
665
|
.file-row-agent-btn.stale { border-color: var(--yellow); color: var(--yellow); }
|
|
656
|
-
.file-row-agent-btn.stale:hover { background: var(--yellow) !important; color:
|
|
666
|
+
.file-row-agent-btn.stale:hover { background: var(--yellow) !important; color: var(--bg) !important; }
|
|
657
667
|
.file-row-fix-btn {
|
|
658
668
|
background: var(--red) !important; border-color: var(--red) !important;
|
|
659
669
|
color: #fff !important; font-weight: 600;
|
|
660
670
|
}
|
|
661
671
|
.file-row-fix-btn:hover { opacity: 0.85; }
|
|
672
|
+
.obs-action-btn {
|
|
673
|
+
display: inline-flex; align-items: center; gap: 3px;
|
|
674
|
+
padding: 2px 7px; font-size: 10px;
|
|
675
|
+
background: transparent; border: 1px solid var(--border); border-radius: 4px;
|
|
676
|
+
color: var(--dim); cursor: pointer; white-space: nowrap; flex-shrink: 0;
|
|
677
|
+
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
678
|
+
}
|
|
679
|
+
.obs-action-btn:hover { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
|
680
|
+
.obs-batch-btn { border-color: var(--yellow); color: var(--yellow); }
|
|
681
|
+
.obs-batch-btn:hover { background: rgba(255,200,0,0.15); color: var(--yellow); border-color: var(--yellow); }
|
|
682
|
+
.obs-expand-btn { background:none; border:none; color:var(--muted); cursor:pointer; font-size:10px; padding:2px 4px; }
|
|
683
|
+
.obs-expand-btn:hover { color:var(--accent); }
|
|
684
|
+
.obs-detail { display:none; padding:6px 0 2px 0; border-top:1px dashed var(--border); margin-top:4px; }
|
|
685
|
+
.obs-detail.open { display:block; }
|
|
686
|
+
.obs-detail-list { max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:2px; }
|
|
687
|
+
.obs-detail-item { display:flex; align-items:center; gap:6px; font-size:11px; color:var(--muted); padding:2px 0; }
|
|
688
|
+
.obs-detail-item input[type="checkbox"] { margin:0; flex-shrink:0; accent-color:var(--accent); }
|
|
689
|
+
.obs-detail-item span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
|
690
|
+
.obs-detail-bar { display:flex; align-items:center; gap:8px; margin-top:6px; padding-top:6px; border-top:1px dashed var(--border); }
|
|
691
|
+
.obs-run-selected { padding:3px 10px; font-size:11px; font-weight:600; background:var(--accent); color:var(--bg); border:none; border-radius:4px; cursor:pointer; }
|
|
692
|
+
.obs-run-selected:hover { opacity:0.85; }
|
|
693
|
+
.obs-run-selected:disabled { opacity:0.4; cursor:not-allowed; }
|
|
694
|
+
.obs-select-all { font-size:10px; color:var(--dim); cursor:pointer; background:none; border:none; }
|
|
695
|
+
.obs-select-all:hover { color:var(--accent); }
|
|
696
|
+
.obs-tier-badge { display:inline-block; padding:1px 6px; border-radius:3px; font-size:10px; font-weight:700; letter-spacing:0.5px; }
|
|
697
|
+
.obs-tier-critical { background:rgba(248,81,73,0.2); color:var(--red); }
|
|
698
|
+
.obs-tier-important { background:rgba(227,179,65,0.2); color:var(--yellow); }
|
|
699
|
+
.obs-tier-normal { background:rgba(139,148,158,0.15); color:var(--muted); }
|
|
700
|
+
.obs-fp-list { font-size:11px; color:var(--muted); margin:4px 0 0 18px; }
|
|
701
|
+
.obs-fp-item { display:flex; gap:6px; padding:1px 0; align-items:baseline; }
|
|
702
|
+
.obs-fp-type { color:var(--yellow); font-weight:600; white-space:nowrap; font-size:10px; }
|
|
703
|
+
.obs-fp-line { color:var(--dim); font-size:10px; flex-shrink:0; }
|
|
704
|
+
.obs-tier-group { margin-bottom:8px; }
|
|
705
|
+
.obs-tier-group-header { display:flex; align-items:center; gap:8px; padding:4px 0; font-size:12px; font-weight:600; color:var(--text); }
|
|
662
706
|
.file-row-err-badge {
|
|
663
707
|
display: inline-flex; align-items: center;
|
|
664
708
|
font-size: 11px; padding: 1px 6px; border-radius: 10px;
|
|
@@ -768,7 +812,7 @@
|
|
|
768
812
|
background: none; border: 1px solid var(--yellow); color: var(--yellow);
|
|
769
813
|
cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
|
|
770
814
|
}
|
|
771
|
-
.agent-panel-cancel:hover { background: var(--yellow); color:
|
|
815
|
+
.agent-panel-cancel:hover { background: var(--yellow); color: var(--bg); }
|
|
772
816
|
.agent-queue-badge {
|
|
773
817
|
font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
|
|
774
818
|
border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
|
|
@@ -1031,6 +1075,8 @@
|
|
|
1031
1075
|
<span class="header-project" id="projectName">—</span>
|
|
1032
1076
|
<span class="header-time" id="scannedAt"></span>
|
|
1033
1077
|
<span class="header-agent-rights" id="headerAgentRights" title="Права/режим выполнения агента">🔐 —</span>
|
|
1078
|
+
<button id="rescanBtn" onclick="rescan()" title="Пересканировать проект и обновить данные">🔄</button>
|
|
1079
|
+
<span id="rescanStatus" class="status-idle"></span>
|
|
1034
1080
|
<button id="runAllBtn" onclick="runAllTests()" title="Запустить все unit и integration тесты">▶ Все тесты</button>
|
|
1035
1081
|
<button id="termBtn" onclick="toggleAgentPanel()" title="Показать/скрыть терминал агента">📟 Terminal</button>
|
|
1036
1082
|
<div style="position:relative">
|
|
@@ -2060,7 +2106,7 @@ async function reauthAgent() {
|
|
|
2060
2106
|
await fetch('/api/agent-reauth', { method: 'POST' });
|
|
2061
2107
|
}
|
|
2062
2108
|
|
|
2063
|
-
async function runAgentTask(task, featureKey, filePath, selectedFilePaths) {
|
|
2109
|
+
async function runAgentTask(task, featureKey, filePath, selectedFilePaths, meta) {
|
|
2064
2110
|
document.getElementById('agentPanel').classList.add('open');
|
|
2065
2111
|
document.getElementById('termBtn').classList.add('term-active');
|
|
2066
2112
|
if (!agentRunning) {
|
|
@@ -2074,10 +2120,60 @@ async function runAgentTask(task, featureKey, filePath, selectedFilePaths) {
|
|
|
2074
2120
|
featureKey,
|
|
2075
2121
|
filePath: filePath || undefined,
|
|
2076
2122
|
selectedFilePaths: Array.isArray(selectedFilePaths) ? selectedFilePaths : undefined,
|
|
2123
|
+
meta: meta || undefined,
|
|
2077
2124
|
}),
|
|
2078
2125
|
});
|
|
2079
2126
|
}
|
|
2080
2127
|
|
|
2128
|
+
// ─── Observability drill-down helpers ─────────────────────────────────────────
|
|
2129
|
+
function toggleObsDetail(id) {
|
|
2130
|
+
const el = document.getElementById('obs-detail-' + id);
|
|
2131
|
+
if (el) el.classList.toggle('open');
|
|
2132
|
+
}
|
|
2133
|
+
|
|
2134
|
+
function obsUpdateSelectedCount(groupId) {
|
|
2135
|
+
const container = document.getElementById('obs-detail-' + groupId);
|
|
2136
|
+
if (!container) return;
|
|
2137
|
+
const checked = container.querySelectorAll('input[type="checkbox"]:checked').length;
|
|
2138
|
+
const btn = container.querySelector('.obs-run-selected');
|
|
2139
|
+
if (btn) {
|
|
2140
|
+
btn.textContent = checked > 0 ? `исправить выбранные (${checked})` : 'исправить выбранные';
|
|
2141
|
+
btn.disabled = checked === 0;
|
|
2142
|
+
}
|
|
2143
|
+
}
|
|
2144
|
+
|
|
2145
|
+
function obsToggleAll(groupId) {
|
|
2146
|
+
const container = document.getElementById('obs-detail-' + groupId);
|
|
2147
|
+
if (!container) return;
|
|
2148
|
+
const boxes = container.querySelectorAll('input[type="checkbox"]');
|
|
2149
|
+
const allChecked = Array.from(boxes).every(b => b.checked);
|
|
2150
|
+
boxes.forEach(b => b.checked = !allChecked);
|
|
2151
|
+
obsUpdateSelectedCount(groupId);
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
function obsRunSelected(groupId, task, baseMeta) {
|
|
2155
|
+
const container = document.getElementById('obs-detail-' + groupId);
|
|
2156
|
+
if (!container) return;
|
|
2157
|
+
const indices = [];
|
|
2158
|
+
container.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
|
|
2159
|
+
indices.push(parseInt(cb.dataset.idx, 10));
|
|
2160
|
+
});
|
|
2161
|
+
if (indices.length === 0) return;
|
|
2162
|
+
runAgentTask('obs-fix-selected', null, null, null, { ...baseMeta, catalogIndices: indices });
|
|
2163
|
+
}
|
|
2164
|
+
|
|
2165
|
+
function obsMissingRunSelected(groupId) {
|
|
2166
|
+
const container = document.getElementById('obs-detail-' + groupId);
|
|
2167
|
+
if (!container) return;
|
|
2168
|
+
const indices = [];
|
|
2169
|
+
container.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
|
|
2170
|
+
const idx = parseInt(cb.dataset.missingIdx, 10);
|
|
2171
|
+
if (!isNaN(idx)) indices.push(idx);
|
|
2172
|
+
});
|
|
2173
|
+
if (indices.length === 0) return;
|
|
2174
|
+
runAgentTask('obs-fix-selected', null, null, null, { missingLogIndices: indices, recommendationType: 'add event' });
|
|
2175
|
+
}
|
|
2176
|
+
|
|
2081
2177
|
async function runTests(featureKey, testType) {
|
|
2082
2178
|
document.getElementById('agentPanel').classList.add('open');
|
|
2083
2179
|
document.getElementById('termBtn').classList.add('term-active');
|
|
@@ -2138,6 +2234,39 @@ function pluralFiles(n) {
|
|
|
2138
2234
|
return 'файлов';
|
|
2139
2235
|
}
|
|
2140
2236
|
|
|
2237
|
+
// ─── Rescan ───────────────────────────────────────────────────────────────────
|
|
2238
|
+
async function rescan() {
|
|
2239
|
+
const btn = document.getElementById('rescanBtn');
|
|
2240
|
+
const statusEl = document.getElementById('rescanStatus');
|
|
2241
|
+
if (!btn || btn.classList.contains('spinning')) return;
|
|
2242
|
+
|
|
2243
|
+
btn.classList.add('spinning');
|
|
2244
|
+
if (statusEl) { statusEl.className = 'status-scanning'; statusEl.textContent = 'Сканирую…'; }
|
|
2245
|
+
|
|
2246
|
+
try {
|
|
2247
|
+
const res = await fetch('/api/rescan', { method: 'POST' });
|
|
2248
|
+
if (!res.ok) throw new Error('rescan failed');
|
|
2249
|
+
D = await res.json();
|
|
2250
|
+
const t = new Date(D.scannedAt).toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
|
2251
|
+
document.getElementById('scannedAt').textContent = t;
|
|
2252
|
+
window.__obsCatalog = D.observability?.catalog || [];
|
|
2253
|
+
renderStats();
|
|
2254
|
+
renderSidebar();
|
|
2255
|
+
renderContent();
|
|
2256
|
+
if (statusEl) {
|
|
2257
|
+
statusEl.className = 'status-done';
|
|
2258
|
+
statusEl.textContent = '✓ актуально · ' + t;
|
|
2259
|
+
// Через 8 секунд приглушаем, но оставляем видимым
|
|
2260
|
+
setTimeout(() => { if (statusEl) { statusEl.className = 'status-idle'; statusEl.textContent = '✓ ' + t; } }, 8000);
|
|
2261
|
+
}
|
|
2262
|
+
} catch (e) {
|
|
2263
|
+
console.error('Rescan error:', e);
|
|
2264
|
+
if (statusEl) { statusEl.className = 'status-error'; statusEl.textContent = '⚠ ошибка сканирования'; }
|
|
2265
|
+
} finally {
|
|
2266
|
+
btn.classList.remove('spinning');
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
|
|
2141
2270
|
// ─── Init ─────────────────────────────────────────────────────────────────────
|
|
2142
2271
|
async function init() {
|
|
2143
2272
|
try {
|
|
@@ -2195,19 +2324,31 @@ function renderStats() {
|
|
|
2195
2324
|
|
|
2196
2325
|
let items;
|
|
2197
2326
|
if (contextMode === 'observability') {
|
|
2198
|
-
const
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
|
|
2327
|
+
const o = D.observability;
|
|
2328
|
+
if (o) {
|
|
2329
|
+
const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
|
|
2330
|
+
const structPct = Math.round(o.metrics.structured_completeness * 100);
|
|
2331
|
+
const v2 = o.missingCriticalLogsV2 || [];
|
|
2332
|
+
const missingCoverage = v2.length || o.missingCriticalLogs.length;
|
|
2333
|
+
const totalFPs = v2.reduce((s,m) => s + m.failurePoints.length, 0);
|
|
2334
|
+
const noisyCount = o.topNoisyPatterns.length;
|
|
2335
|
+
items = [
|
|
2336
|
+
{ v: o.catalog.length, l: 'Источники логов' },
|
|
2337
|
+
{ v: noiseRatio + '%', l: 'Коэффициент шума', c: noiseRatio > 30 ? '#f85149' : noiseRatio > 10 ? '#e3b341' : undefined },
|
|
2338
|
+
{ v: structPct + '%', l: 'Структурированность', c: structPct < 50 ? '#f85149' : structPct < 80 ? '#e3b341' : undefined },
|
|
2339
|
+
{ v: totalFPs || missingCoverage, l: totalFPs ? 'Точек отказа' : 'Нет покрытия', c: totalFPs > 10 ? '#f85149' : (totalFPs || missingCoverage) ? '#e3b341' : undefined },
|
|
2340
|
+
{ v: noisyCount, l: 'Шумных паттернов', c: noisyCount ? '#e3b341' : undefined },
|
|
2341
|
+
];
|
|
2342
|
+
} else {
|
|
2343
|
+
const serviceSources = src.filter(m => m.type === 'service' || m.type === 'util' || m.type === 'other');
|
|
2344
|
+
items = [
|
|
2345
|
+
{ v: serviceSources.length, l: 'Источники логов' },
|
|
2346
|
+
{ v: '—', l: 'Коэффициент шума' },
|
|
2347
|
+
{ v: '—', l: 'Структурированность' },
|
|
2348
|
+
{ v: '—', l: 'Нет покрытия' },
|
|
2349
|
+
{ v: '—', l: 'Шумных паттернов' },
|
|
2350
|
+
];
|
|
2351
|
+
}
|
|
2211
2352
|
} else if (D.hasConfig && D.features) {
|
|
2212
2353
|
const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
|
|
2213
2354
|
items = [
|
|
@@ -2326,57 +2467,262 @@ function renderQaOnboarding() {
|
|
|
2326
2467
|
}
|
|
2327
2468
|
|
|
2328
2469
|
function renderObservability(c) {
|
|
2329
|
-
const
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2470
|
+
const o = D.observability;
|
|
2471
|
+
if (!o) {
|
|
2472
|
+
c.innerHTML = `<div class="onboarding-block"><h3>Наблюдаемость</h3><p>Данные анализа логов недоступны. Пересканируй проект.</p></div>`;
|
|
2473
|
+
return;
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
|
|
2477
|
+
const structPct = Math.round(o.metrics.structured_completeness * 100);
|
|
2478
|
+
const actionPct = Math.round(o.metrics.error_actionability * 100);
|
|
2479
|
+
const coveragePct = Math.round(o.metrics.coverage_of_key_flows * 100);
|
|
2480
|
+
|
|
2481
|
+
function metricColor(val, goodThreshold, warnThreshold, invert) {
|
|
2482
|
+
// invert=true: higher is worse (noise), invert=false: higher is better (coverage)
|
|
2483
|
+
if (invert) return val > warnThreshold ? 'var(--red)' : val > goodThreshold ? 'var(--yellow)' : 'var(--green)';
|
|
2484
|
+
return val < warnThreshold ? 'var(--red)' : val < goodThreshold ? 'var(--yellow)' : 'var(--green)';
|
|
2485
|
+
}
|
|
2486
|
+
|
|
2487
|
+
const formatLabels = { structured: 'structured', mixed: 'mixed', unstructured: 'unstructured' };
|
|
2488
|
+
const sourceByFormat = [
|
|
2489
|
+
{ label: 'Structured', count: o.catalog.filter(c => c.format === 'structured').length, color: 'var(--green)' },
|
|
2490
|
+
{ label: 'Mixed', count: o.catalog.filter(c => c.format === 'mixed').length, color: 'var(--yellow)' },
|
|
2491
|
+
{ label: 'Unstructured', count: o.catalog.filter(c => c.format === 'unstructured').length, color: 'var(--red)' },
|
|
2334
2492
|
].filter(x => x.count > 0);
|
|
2335
|
-
|
|
2336
|
-
const
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2493
|
+
|
|
2494
|
+
const recLabels = {
|
|
2495
|
+
'suppress': 'убрать',
|
|
2496
|
+
'downgrade level': 'понизить уровень',
|
|
2497
|
+
'enrich fields': 'обогатить поля',
|
|
2498
|
+
'add event': 'добавить событие',
|
|
2499
|
+
};
|
|
2500
|
+
const hasAgent = !!D.agent;
|
|
2501
|
+
|
|
2502
|
+
// Store catalog for buttons to reference by index (avoids inline JSON in onclick)
|
|
2503
|
+
window.__obsCatalog = o.catalog;
|
|
2504
|
+
|
|
2505
|
+
const noisyRows = o.topNoisyPatterns.slice(0, 8).map(i => {
|
|
2506
|
+
const safePattern = escapeHtml(i.pattern).replace(/'/g, ''');
|
|
2507
|
+
const btn = hasAgent
|
|
2508
|
+
? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-suppress-pattern',null,null,null,{pattern:'${safePattern}',recommendation:'${i.recommendation}'})">убрать</button>`
|
|
2509
|
+
: '';
|
|
2510
|
+
return `
|
|
2511
|
+
<div class="obs-list-item">
|
|
2512
|
+
<span class="obs-priority-${i.priority}" style="flex-shrink:0">[${i.priority}]</span>
|
|
2513
|
+
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(i.pattern)}">${escapeHtml(i.pattern)}</span>
|
|
2514
|
+
<strong style="flex-shrink:0;color:var(--red)">×${i.count} → ${recLabels[i.recommendation] || i.recommendation}</strong>
|
|
2515
|
+
${btn}
|
|
2516
|
+
</div>`;
|
|
2517
|
+
}).join('') || '<div class="obs-sub">Шумных паттернов не обнаружено</div>';
|
|
2518
|
+
|
|
2519
|
+
// ── Missing critical logs (V2: grouped by risk tier with failure points) ──
|
|
2520
|
+
const v2Data = o.missingCriticalLogsV2 || [];
|
|
2521
|
+
window.__obsMissingV2 = v2Data;
|
|
2522
|
+
const fpTypeLabels = {
|
|
2523
|
+
'empty-catch':'пустой catch','catch-no-log':'catch без лога','promise-catch-no-log':'.catch без лога',
|
|
2524
|
+
'http-no-error-handling':'HTTP без обработки','db-no-error-handling':'DB без обработки',
|
|
2525
|
+
'throw-no-log':'throw без лога','error-check-no-log':'if(err) без лога',
|
|
2526
|
+
};
|
|
2527
|
+
let missingSection = '';
|
|
2528
|
+
if (v2Data.length > 0) {
|
|
2529
|
+
const tierLabels = { critical:'Критичные', important:'Важные', normal:'Обычные' };
|
|
2530
|
+
const tiers = ['critical','important','normal'];
|
|
2531
|
+
const tierSections = tiers.map(tier => {
|
|
2532
|
+
const items = v2Data.filter(m => m.riskTier === tier);
|
|
2533
|
+
if (!items.length) return '';
|
|
2534
|
+
const groupId = 'missing-' + tier;
|
|
2535
|
+
const expandBtn = hasAgent ? `<button class="obs-expand-btn" onclick="event.stopPropagation();toggleObsDetail('${groupId}')">▼</button>` : '';
|
|
2536
|
+
const totalFPs = items.reduce((s,m) => s + m.failurePoints.length, 0);
|
|
2537
|
+
const detailItems = items.map(m => {
|
|
2538
|
+
const globalIdx = v2Data.indexOf(m);
|
|
2539
|
+
const fpCount = m.failurePoints.length;
|
|
2540
|
+
const fpPreview = m.failurePoints.slice(0,3).map(fp =>
|
|
2541
|
+
`<div class="obs-fp-item"><span class="obs-fp-type">${fpTypeLabels[fp.type]||fp.type}</span><span class="obs-fp-line">~${fp.lineApprox}</span></div>`
|
|
2542
|
+
).join('');
|
|
2543
|
+
const moreCount = fpCount > 3 ? `<div class="obs-fp-item" style="color:var(--dim)">...и ещё ${fpCount-3}</div>` : '';
|
|
2544
|
+
const covBadge = m.hasAnyWarnError ? '<span style="font-size:10px;color:var(--dim)" title="Есть warn/error, но не все точки покрыты">частично</span>' : '';
|
|
2545
|
+
return `<label class="obs-detail-item" style="flex-wrap:wrap">
|
|
2546
|
+
<input type="checkbox" data-missing-idx="${globalIdx}" onchange="obsUpdateSelectedCount('${groupId}')">
|
|
2547
|
+
<span title="${escapeHtml(m.modulePath)}" style="flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(m.modulePath.split('/').slice(-2).join('/'))}</span>
|
|
2548
|
+
<span style="color:var(--dim);flex-shrink:0;font-size:10px">${escapeHtml(m.roleHint)}</span>
|
|
2549
|
+
<span style="color:var(--yellow);flex-shrink:0">${fpCount} точек</span>
|
|
2550
|
+
${covBadge}
|
|
2551
|
+
${fpPreview || moreCount ? `<div class="obs-fp-list" style="width:100%">${fpPreview}${moreCount}</div>` : ''}
|
|
2552
|
+
</label>`;
|
|
2553
|
+
}).join('');
|
|
2554
|
+
const addBtn = hasAgent ? `<button class="obs-run-selected" disabled onclick="obsMissingRunSelected('${groupId}')">добавить логи выбранным</button>` : '';
|
|
2555
|
+
const detail = hasAgent ? `
|
|
2556
|
+
<div id="obs-detail-${groupId}" class="obs-detail">
|
|
2557
|
+
<div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
|
|
2558
|
+
<button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
|
|
2559
|
+
</div>
|
|
2560
|
+
<div class="obs-detail-list">${detailItems}</div>
|
|
2561
|
+
<div class="obs-detail-bar">${addBtn}</div>
|
|
2562
|
+
</div>` : '';
|
|
2563
|
+
return `<div class="obs-tier-group">
|
|
2564
|
+
<div class="obs-tier-group-header">
|
|
2565
|
+
<span class="obs-tier-badge obs-tier-${tier}">${tierLabels[tier]}</span>
|
|
2566
|
+
<span>${items.length} модулей, ${totalFPs} точек отказа</span>
|
|
2567
|
+
${expandBtn}
|
|
2568
|
+
</div>
|
|
2569
|
+
${detail}
|
|
2570
|
+
</div>`;
|
|
2571
|
+
}).join('');
|
|
2572
|
+
missingSection = tierSections || '<div class="obs-sub" style="color:var(--green)">Критичные сценарии покрыты</div>';
|
|
2573
|
+
} else {
|
|
2574
|
+
missingSection = '<div class="obs-sub" style="color:var(--green)">Критичные сценарии покрыты</div>';
|
|
2575
|
+
}
|
|
2576
|
+
|
|
2577
|
+
const fieldGaps = o.fieldGaps || {};
|
|
2578
|
+
const fieldGapEntries = Object.entries(fieldGaps).filter(([,v]) => v > 0).sort((a,b) => b[1] - a[1]);
|
|
2579
|
+
const fieldGapRows = fieldGapEntries.map(([name, count]) => {
|
|
2580
|
+
const groupId = 'field-' + name.replace(/[^a-z0-9]/gi, '_');
|
|
2581
|
+
const affectedItems = o.catalog.filter(c => (c.missingFields||[]).includes(name));
|
|
2582
|
+
const expandBtn = hasAgent ? `<button class="obs-expand-btn" onclick="event.stopPropagation();toggleObsDetail('${groupId}')">▼</button>` : '';
|
|
2583
|
+
const detailItems = affectedItems.map(ci => {
|
|
2584
|
+
const catIdx = o.catalog.indexOf(ci);
|
|
2585
|
+
return `<label class="obs-detail-item"><input type="checkbox" data-idx="${catIdx}" onchange="obsUpdateSelectedCount('${groupId}')"><span title="${escapeHtml(ci.modulePath)}">${escapeHtml(ci.modulePath.split('/').slice(-2).join('/'))}</span><span style="color:var(--dim);flex-shrink:0">${ci.format}</span></label>`;
|
|
2586
|
+
}).join('');
|
|
2587
|
+
const detail = hasAgent ? `
|
|
2588
|
+
<div id="obs-detail-${groupId}" class="obs-detail">
|
|
2589
|
+
<div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
|
|
2590
|
+
<button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
|
|
2591
|
+
</div>
|
|
2592
|
+
<div class="obs-detail-list">${detailItems}</div>
|
|
2593
|
+
<div class="obs-detail-bar">
|
|
2594
|
+
<button class="obs-run-selected" disabled onclick="obsRunSelected('${groupId}','obs-fix-selected',{fieldName:'${escapeHtml(name)}'})">обогатить выбранные</button>
|
|
2595
|
+
</div>
|
|
2596
|
+
</div>` : '';
|
|
2597
|
+
return `<div>
|
|
2598
|
+
<div class="obs-list-item">
|
|
2599
|
+
<span><code>${escapeHtml(name)}</code></span>
|
|
2600
|
+
<strong style="color:${count > 20 ? 'var(--red)' : count > 5 ? 'var(--yellow)' : 'var(--muted)'}">${count} пропусков</strong>
|
|
2601
|
+
${expandBtn}
|
|
2602
|
+
</div>
|
|
2603
|
+
${detail}
|
|
2604
|
+
</div>`;
|
|
2605
|
+
}).join('') || '<div class="obs-sub" style="color:var(--green)">Все обязательные поля на месте</div>';
|
|
2606
|
+
|
|
2607
|
+
const catalogRows = o.catalog.slice(0, 15).map((i, idx) => {
|
|
2608
|
+
const missing = (i.missingFields || []);
|
|
2609
|
+
const missingStr = missing.length ? missing.join(', ') : '—';
|
|
2610
|
+
const btn = hasAgent
|
|
2611
|
+
? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-fix-module',null,null,null,{catalogIndex:${idx}})">исправить</button>`
|
|
2612
|
+
: '';
|
|
2613
|
+
return `
|
|
2614
|
+
<div class="obs-cat-row">
|
|
2615
|
+
<span title="${escapeHtml(i.modulePath)}">${escapeHtml(i.modulePath.split('/').slice(-2).join('/'))}</span>
|
|
2616
|
+
<span>${i.level}</span>
|
|
2617
|
+
<span style="color:${i.format==='structured'?'var(--green)':i.format==='mixed'?'var(--yellow)':'var(--red)'}">${i.format}</span>
|
|
2618
|
+
<span style="color:${missing.length > 4 ? 'var(--red)' : missing.length > 0 ? 'var(--yellow)' : 'var(--green)'}">${missing.length}/8</span>
|
|
2619
|
+
<span title="${escapeHtml(missingStr)}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(missingStr)}</span>
|
|
2620
|
+
<span style="color:${i.recommendation==='suppress'?'var(--red)':i.recommendation==='add event'?'var(--yellow)':'var(--muted)'}">${recLabels[i.recommendation] || i.recommendation}</span>
|
|
2621
|
+
${btn}
|
|
2622
|
+
</div>`}).join('');
|
|
2341
2623
|
|
|
2342
2624
|
c.innerHTML = `
|
|
2343
2625
|
<div class="onboarding-block">
|
|
2344
2626
|
<h3>Наблюдаемость: что это?</h3>
|
|
2345
|
-
<p
|
|
2627
|
+
<p>Аудит покрытия логами: что добавить, что убрать, что обогатить — на основе статического анализа лог-вызовов.</p>
|
|
2628
|
+
</div>
|
|
2629
|
+
|
|
2630
|
+
<div class="obs-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
|
|
2631
|
+
<div class="obs-card">
|
|
2632
|
+
<div class="obs-title">Коэффициент шума</div>
|
|
2633
|
+
<div class="obs-value" style="color:${metricColor(noiseRatio,10,30,true)}">${noiseRatio}%</div>
|
|
2634
|
+
<div class="obs-sub">Доля шумных лог-вызовов из всех.</div>
|
|
2635
|
+
</div>
|
|
2636
|
+
<div class="obs-card">
|
|
2637
|
+
<div class="obs-title">Структурированность</div>
|
|
2638
|
+
<div class="obs-value" style="color:${metricColor(structPct,80,50,false)}">${structPct}%</div>
|
|
2639
|
+
<div class="obs-sub">Логи с обязательными полями (module, event, traceId).</div>
|
|
2640
|
+
</div>
|
|
2641
|
+
<div class="obs-card">
|
|
2642
|
+
<div class="obs-title">Actionable ошибки</div>
|
|
2643
|
+
<div class="obs-value" style="color:${metricColor(actionPct,80,50,false)}">${actionPct}%</div>
|
|
2644
|
+
<div class="obs-sub">ERROR-логи с контекстом для диагностики.</div>
|
|
2645
|
+
</div>
|
|
2646
|
+
<div class="obs-card">
|
|
2647
|
+
<div class="obs-title">Покрытие сценариев</div>
|
|
2648
|
+
<div class="obs-value" style="color:${metricColor(coveragePct,80,50,false)}">${coveragePct}%</div>
|
|
2649
|
+
<div class="obs-sub">Модули с хотя бы одним warn/error событием.</div>
|
|
2650
|
+
</div>
|
|
2346
2651
|
</div>
|
|
2347
2652
|
|
|
2348
|
-
<div class="obs-grid">
|
|
2653
|
+
<div class="obs-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:12px">
|
|
2349
2654
|
<div class="obs-card">
|
|
2350
|
-
<div class="obs-title">Источники
|
|
2655
|
+
<div class="obs-title">Источники по формату</div>
|
|
2351
2656
|
<div class="obs-list">
|
|
2352
|
-
${
|
|
2657
|
+
${sourceByFormat.map(g => `
|
|
2658
|
+
<div class="obs-list-item">
|
|
2659
|
+
<span style="color:${g.color}">${g.label}</span>
|
|
2660
|
+
<strong>${g.count}</strong>
|
|
2661
|
+
</div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
|
|
2353
2662
|
</div>
|
|
2354
2663
|
</div>
|
|
2355
|
-
|
|
2356
2664
|
<div class="obs-card">
|
|
2357
|
-
<div class="obs-title"
|
|
2665
|
+
<div class="obs-title">Классификация логов</div>
|
|
2358
2666
|
<div class="obs-list">
|
|
2359
|
-
|
|
2667
|
+
<div class="obs-list-item"><span style="color:var(--red)">Мусор (убрать)</span><strong>${o.classification.trash}</strong></div>
|
|
2668
|
+
<div class="obs-list-item"><span style="color:var(--green)">Полезные</span><strong>${o.classification.useful}</strong></div>
|
|
2669
|
+
<div class="obs-list-item"><span style="color:var(--blue)">Критичные</span><strong>${o.classification.critical}</strong></div>
|
|
2360
2670
|
</div>
|
|
2361
2671
|
</div>
|
|
2362
|
-
|
|
2363
2672
|
<div class="obs-card">
|
|
2364
|
-
<div class="obs-title"
|
|
2365
|
-
<div class="obs-
|
|
2366
|
-
|
|
2673
|
+
<div class="obs-title">Рекомендации</div>
|
|
2674
|
+
<div class="obs-list" style="gap:2px">
|
|
2675
|
+
${['suppress','enrich fields','add event','downgrade level'].map(rec => {
|
|
2676
|
+
const items = o.catalog.filter(c => c.recommendation === rec);
|
|
2677
|
+
if (!items.length) return '';
|
|
2678
|
+
const groupId = 'rec-' + rec.replace(/\s+/g, '-');
|
|
2679
|
+
const expandBtn = hasAgent ? `<button class="obs-expand-btn" onclick="event.stopPropagation();toggleObsDetail('${groupId}')">▼</button>` : '';
|
|
2680
|
+
const detailItems = items.map((ci, i) => {
|
|
2681
|
+
const catIdx = o.catalog.indexOf(ci);
|
|
2682
|
+
const mf = (ci.missingFields||[]).join(', ') || '—';
|
|
2683
|
+
return `<label class="obs-detail-item"><input type="checkbox" data-idx="${catIdx}" onchange="obsUpdateSelectedCount('${groupId}')"><span title="${escapeHtml(ci.modulePath)}">${escapeHtml(ci.modulePath.split('/').slice(-2).join('/'))}</span><span style="color:var(--dim);flex-shrink:0">${ci.format}</span></label>`;
|
|
2684
|
+
}).join('');
|
|
2685
|
+
const detail = hasAgent ? `
|
|
2686
|
+
<div id="obs-detail-${groupId}" class="obs-detail">
|
|
2687
|
+
<div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
|
|
2688
|
+
<button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
|
|
2689
|
+
</div>
|
|
2690
|
+
<div class="obs-detail-list">${detailItems}</div>
|
|
2691
|
+
<div class="obs-detail-bar">
|
|
2692
|
+
<button class="obs-run-selected" disabled onclick="obsRunSelected('${groupId}','obs-fix-selected',{recommendationType:'${rec}'})">исправить выбранные</button>
|
|
2693
|
+
</div>
|
|
2694
|
+
</div>` : '';
|
|
2695
|
+
return `<div>
|
|
2696
|
+
<div class="obs-list-item"><span>${recLabels[rec]}</span><strong>${items.length}</strong>${expandBtn}</div>
|
|
2697
|
+
${detail}
|
|
2698
|
+
</div>`;
|
|
2699
|
+
}).join('')}
|
|
2700
|
+
</div>
|
|
2367
2701
|
</div>
|
|
2702
|
+
</div>
|
|
2368
2703
|
|
|
2704
|
+
<div class="obs-grid" style="grid-template-columns:1fr 1fr;margin-bottom:12px">
|
|
2369
2705
|
<div class="obs-card">
|
|
2370
|
-
<div class="obs-title"
|
|
2371
|
-
<div class="obs-
|
|
2372
|
-
<div class="obs-sub">Сумма упавших тестов из последнего прогона как индикатор нестабильности сигналов.</div>
|
|
2706
|
+
<div class="obs-title">Что убрать — шумные паттерны</div>
|
|
2707
|
+
<div class="obs-list" style="gap:4px">${noisyRows}</div>
|
|
2373
2708
|
</div>
|
|
2709
|
+
<div class="obs-card">
|
|
2710
|
+
<div class="obs-title">Что добавить — нет критичных логов</div>
|
|
2711
|
+
<div class="obs-list" style="gap:4px">${missingSection}</div>
|
|
2712
|
+
</div>
|
|
2713
|
+
</div>
|
|
2374
2714
|
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2715
|
+
<div class="obs-card" style="margin-bottom:12px">
|
|
2716
|
+
<div class="obs-title">Что обогатить — пробелы по полям</div>
|
|
2717
|
+
<div class="obs-sub" style="margin-bottom:8px">Обязательные поля по стандарту: service, env, trace_id, request_id, event_name, outcome, error_code (warn/error), user_id.</div>
|
|
2718
|
+
<div class="obs-list">${fieldGapRows}</div>
|
|
2719
|
+
</div>
|
|
2720
|
+
|
|
2721
|
+
<div class="obs-card" style="margin-bottom:12px">
|
|
2722
|
+
<div class="obs-title">Каталог источников логов (топ 15)</div>
|
|
2723
|
+
<div class="obs-catalog" style="margin-top:8px;border:none;padding:0">
|
|
2724
|
+
<div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>пробелы</span><span>не хватает</span><span>действие</span><span></span></div>
|
|
2725
|
+
${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
|
|
2380
2726
|
</div>
|
|
2381
2727
|
</div>`;
|
|
2382
2728
|
}
|
|
@@ -2410,9 +2756,19 @@ function renderObservabilityOverview(c) {
|
|
|
2410
2756
|
`<div class="obs-row"><span class="obs-priority-${i.priority}">[${i.priority}]</span> ${escapeHtml(i.pattern)} · x${i.count} → <b>${i.recommendation}</b></div>`
|
|
2411
2757
|
).join('') || '<div class="obs-row">Нет шумных паттернов</div>';
|
|
2412
2758
|
|
|
2413
|
-
const
|
|
2414
|
-
|
|
2415
|
-
|
|
2759
|
+
const v2Missing = o.missingCriticalLogsV2 || [];
|
|
2760
|
+
let missing;
|
|
2761
|
+
if (v2Missing.length) {
|
|
2762
|
+
const tierLabels = { critical: '🔴 крит', important: '🟡 важн', normal: '⚪ обычн' };
|
|
2763
|
+
missing = v2Missing.slice(0, 8).map(m =>
|
|
2764
|
+
`<div class="obs-row"><span class="obs-tier-badge obs-tier-${m.riskTier}">${tierLabels[m.riskTier]}</span> ${escapeHtml(m.modulePath)} · ${m.failurePoints.length} точек отказа</div>`
|
|
2765
|
+
).join('');
|
|
2766
|
+
if (v2Missing.length > 8) missing += `<div class="obs-row" style="color:var(--dim)">...и ещё ${v2Missing.length - 8}</div>`;
|
|
2767
|
+
} else {
|
|
2768
|
+
missing = (o.missingCriticalLogs || []).slice(0, 5).map(i =>
|
|
2769
|
+
`<div class="obs-row"><span class="obs-priority-${i.priority}">[${i.priority}]</span> ${escapeHtml(i.pattern)} → <b>${i.recommendation}</b></div>`
|
|
2770
|
+
).join('') || '<div class="obs-row">Критичные логи покрыты</div>';
|
|
2771
|
+
}
|
|
2416
2772
|
|
|
2417
2773
|
const catalogRows = (o.catalog || []).slice(0, 10).map(i =>
|
|
2418
2774
|
`<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>`
|
|
@@ -3062,7 +3418,7 @@ function renderUnmappedDetail(c) {
|
|
|
3062
3418
|
<div style="padding:0 0 12px;display:flex;gap:8px;flex-wrap:wrap">
|
|
3063
3419
|
${D.agent ? `<button id="runAgentUnmapped" style="
|
|
3064
3420
|
padding:7px 14px; background:var(--blue); border:none;
|
|
3065
|
-
border-radius:6px; color
|
|
3421
|
+
border-radius:6px; color:var(--bg); font-size:12px; font-weight:700; cursor:pointer;
|
|
3066
3422
|
">▶ Разобрать через ${D.agent === 'claude' ? 'Claude Code' : 'Codex'}</button>` : ''}
|
|
3067
3423
|
<button id="copyUnmappedDrill" style="
|
|
3068
3424
|
padding:7px 14px; background:var(--bg-card); border:1px solid var(--border);
|