viberadar 0.3.70 → 0.3.72

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.
@@ -19,7 +19,6 @@
19
19
  --green: #3fb950;
20
20
  --red: #f85149;
21
21
  --yellow: #e3b341;
22
- --accent: #58a6ff;
23
22
  }
24
23
 
25
24
  body {
@@ -46,10 +45,6 @@
46
45
  header h1 { font-size: 18px; font-weight: 700; letter-spacing: -0.3px; }
47
46
  .header-project { margin-left: auto; font-size: 13px; color: var(--muted); }
48
47
  .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
48
  .header-agent-rights {
54
49
  font-size: 11px;
55
50
  color: var(--muted);
@@ -196,7 +191,7 @@
196
191
  .obs-priority-low { color: var(--green); }
197
192
  .obs-catalog { margin-top:10px; background: var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px; }
198
193
  .obs-catalog h4 { font-size:12px; margin-bottom:6px; }
199
- .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); }
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); }
200
195
  .obs-cat-row.head { color: var(--text); font-weight:600; text-transform:uppercase; font-size:10px; }
201
196
 
202
197
  /* ── Layout ──────────────────────────────────────────────────────────────── */
@@ -656,38 +651,14 @@
656
651
  transition: background 0.1s, color 0.1s, border-color 0.1s;
657
652
  }
658
653
  .file-row:hover .file-row-agent-btn { border-color: var(--accent); color: var(--accent); }
659
- .file-row-agent-btn:hover { background: var(--accent); color: var(--bg) !important; border-color: var(--accent); }
654
+ .file-row-agent-btn:hover { background: var(--accent); color: #000 !important; border-color: var(--accent); }
660
655
  .file-row-agent-btn.stale { border-color: var(--yellow); color: var(--yellow); }
661
- .file-row-agent-btn.stale:hover { background: var(--yellow) !important; color: var(--bg) !important; }
656
+ .file-row-agent-btn.stale:hover { background: var(--yellow) !important; color: #000 !important; }
662
657
  .file-row-fix-btn {
663
658
  background: var(--red) !important; border-color: var(--red) !important;
664
659
  color: #fff !important; font-weight: 600;
665
660
  }
666
661
  .file-row-fix-btn:hover { opacity: 0.85; }
667
- .obs-action-btn {
668
- display: inline-flex; align-items: center; gap: 3px;
669
- padding: 2px 7px; font-size: 10px;
670
- background: transparent; border: 1px solid var(--border); border-radius: 4px;
671
- color: var(--dim); cursor: pointer; white-space: nowrap; flex-shrink: 0;
672
- transition: background 0.1s, color 0.1s, border-color 0.1s;
673
- }
674
- .obs-action-btn:hover { background: var(--accent); color: var(--bg); border-color: var(--accent); }
675
- .obs-batch-btn { border-color: var(--yellow); color: var(--yellow); }
676
- .obs-batch-btn:hover { background: rgba(255,200,0,0.15); color: var(--yellow); border-color: var(--yellow); }
677
- .obs-expand-btn { background:none; border:none; color:var(--muted); cursor:pointer; font-size:10px; padding:2px 4px; }
678
- .obs-expand-btn:hover { color:var(--accent); }
679
- .obs-detail { display:none; padding:6px 0 2px 0; border-top:1px dashed var(--border); margin-top:4px; }
680
- .obs-detail.open { display:block; }
681
- .obs-detail-list { max-height:220px; overflow-y:auto; display:flex; flex-direction:column; gap:2px; }
682
- .obs-detail-item { display:flex; align-items:center; gap:6px; font-size:11px; color:var(--muted); padding:2px 0; }
683
- .obs-detail-item input[type="checkbox"] { margin:0; flex-shrink:0; accent-color:var(--accent); }
684
- .obs-detail-item span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
685
- .obs-detail-bar { display:flex; align-items:center; gap:8px; margin-top:6px; padding-top:6px; border-top:1px dashed var(--border); }
686
- .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; }
687
- .obs-run-selected:hover { opacity:0.85; }
688
- .obs-run-selected:disabled { opacity:0.4; cursor:not-allowed; }
689
- .obs-select-all { font-size:10px; color:var(--dim); cursor:pointer; background:none; border:none; }
690
- .obs-select-all:hover { color:var(--accent); }
691
662
  .file-row-err-badge {
692
663
  display: inline-flex; align-items: center;
693
664
  font-size: 11px; padding: 1px 6px; border-radius: 10px;
@@ -797,7 +768,7 @@
797
768
  background: none; border: 1px solid var(--yellow); color: var(--yellow);
798
769
  cursor: pointer; font-size: 11px; padding: 2px 8px; border-radius: 4px;
799
770
  }
800
- .agent-panel-cancel:hover { background: var(--yellow); color: var(--bg); }
771
+ .agent-panel-cancel:hover { background: var(--yellow); color: #000; }
801
772
  .agent-queue-badge {
802
773
  font-size: 11px; color: var(--yellow); background: rgba(255,200,0,0.1);
803
774
  border: 1px solid var(--yellow); border-radius: 4px; padding: 2px 8px;
@@ -1060,7 +1031,6 @@
1060
1031
  <span class="header-project" id="projectName">—</span>
1061
1032
  <span class="header-time" id="scannedAt"></span>
1062
1033
  <span class="header-agent-rights" id="headerAgentRights" title="Права/режим выполнения агента">🔐 —</span>
1063
- <button id="rescanBtn" onclick="rescan()" title="Пересканировать проект и обновить данные">🔄</button>
1064
1034
  <button id="runAllBtn" onclick="runAllTests()" title="Запустить все unit и integration тесты">▶ Все тесты</button>
1065
1035
  <button id="termBtn" onclick="toggleAgentPanel()" title="Показать/скрыть терминал агента">📟 Terminal</button>
1066
1036
  <div style="position:relative">
@@ -2090,7 +2060,7 @@ async function reauthAgent() {
2090
2060
  await fetch('/api/agent-reauth', { method: 'POST' });
2091
2061
  }
2092
2062
 
2093
- async function runAgentTask(task, featureKey, filePath, selectedFilePaths, meta) {
2063
+ async function runAgentTask(task, featureKey, filePath, selectedFilePaths) {
2094
2064
  document.getElementById('agentPanel').classList.add('open');
2095
2065
  document.getElementById('termBtn').classList.add('term-active');
2096
2066
  if (!agentRunning) {
@@ -2104,48 +2074,10 @@ async function runAgentTask(task, featureKey, filePath, selectedFilePaths, meta)
2104
2074
  featureKey,
2105
2075
  filePath: filePath || undefined,
2106
2076
  selectedFilePaths: Array.isArray(selectedFilePaths) ? selectedFilePaths : undefined,
2107
- meta: meta || undefined,
2108
2077
  }),
2109
2078
  });
2110
2079
  }
2111
2080
 
2112
- // ─── Observability drill-down helpers ─────────────────────────────────────────
2113
- function toggleObsDetail(id) {
2114
- const el = document.getElementById('obs-detail-' + id);
2115
- if (el) el.classList.toggle('open');
2116
- }
2117
-
2118
- function obsUpdateSelectedCount(groupId) {
2119
- const container = document.getElementById('obs-detail-' + groupId);
2120
- if (!container) return;
2121
- const checked = container.querySelectorAll('input[type="checkbox"]:checked').length;
2122
- const btn = container.querySelector('.obs-run-selected');
2123
- if (btn) {
2124
- btn.textContent = checked > 0 ? `исправить выбранные (${checked})` : 'исправить выбранные';
2125
- btn.disabled = checked === 0;
2126
- }
2127
- }
2128
-
2129
- function obsToggleAll(groupId) {
2130
- const container = document.getElementById('obs-detail-' + groupId);
2131
- if (!container) return;
2132
- const boxes = container.querySelectorAll('input[type="checkbox"]');
2133
- const allChecked = Array.from(boxes).every(b => b.checked);
2134
- boxes.forEach(b => b.checked = !allChecked);
2135
- obsUpdateSelectedCount(groupId);
2136
- }
2137
-
2138
- function obsRunSelected(groupId, task, baseMeta) {
2139
- const container = document.getElementById('obs-detail-' + groupId);
2140
- if (!container) return;
2141
- const indices = [];
2142
- container.querySelectorAll('input[type="checkbox"]:checked').forEach(cb => {
2143
- indices.push(parseInt(cb.dataset.idx, 10));
2144
- });
2145
- if (indices.length === 0) return;
2146
- runAgentTask('obs-fix-selected', null, null, null, { ...baseMeta, catalogIndices: indices });
2147
- }
2148
-
2149
2081
  async function runTests(featureKey, testType) {
2150
2082
  document.getElementById('agentPanel').classList.add('open');
2151
2083
  document.getElementById('termBtn').classList.add('term-active');
@@ -2206,30 +2138,6 @@ function pluralFiles(n) {
2206
2138
  return 'файлов';
2207
2139
  }
2208
2140
 
2209
- // ─── Rescan ───────────────────────────────────────────────────────────────────
2210
- async function rescan() {
2211
- const btn = document.getElementById('rescanBtn');
2212
- if (!btn || btn.classList.contains('spinning')) return;
2213
- btn.classList.add('spinning');
2214
- btn.title = 'Сканирование…';
2215
- try {
2216
- const res = await fetch('/api/rescan', { method: 'POST' });
2217
- if (!res.ok) throw new Error('rescan failed');
2218
- D = await res.json();
2219
- document.getElementById('scannedAt').textContent =
2220
- new Date(D.scannedAt).toLocaleTimeString();
2221
- window.__obsCatalog = D.observability?.catalog || [];
2222
- renderStats();
2223
- renderSidebar();
2224
- renderContent();
2225
- } catch (e) {
2226
- console.error('Rescan error:', e);
2227
- } finally {
2228
- btn.classList.remove('spinning');
2229
- btn.title = 'Пересканировать проект и обновить данные';
2230
- }
2231
- }
2232
-
2233
2141
  // ─── Init ─────────────────────────────────────────────────────────────────────
2234
2142
  async function init() {
2235
2143
  try {
@@ -2287,29 +2195,19 @@ function renderStats() {
2287
2195
 
2288
2196
  let items;
2289
2197
  if (contextMode === 'observability') {
2290
- const o = D.observability;
2291
- if (o) {
2292
- const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
2293
- const structPct = Math.round(o.metrics.structured_completeness * 100);
2294
- const missingCoverage = o.missingCriticalLogs.length;
2295
- const noisyCount = o.topNoisyPatterns.length;
2296
- items = [
2297
- { v: o.catalog.length, l: 'Источники логов' },
2298
- { v: noiseRatio + '%', l: 'Коэффициент шума', c: noiseRatio > 30 ? '#f85149' : noiseRatio > 10 ? '#e3b341' : undefined },
2299
- { v: structPct + '%', l: 'Структурированность', c: structPct < 50 ? '#f85149' : structPct < 80 ? '#e3b341' : undefined },
2300
- { v: missingCoverage, l: 'Нет покрытия', c: missingCoverage ? '#e3b341' : undefined },
2301
- { v: noisyCount, l: 'Шумных паттернов', c: noisyCount ? '#e3b341' : undefined },
2302
- ];
2303
- } else {
2304
- const serviceSources = src.filter(m => m.type === 'service' || m.type === 'util' || m.type === 'other');
2305
- items = [
2306
- { v: serviceSources.length, l: 'Источники логов' },
2307
- { v: '—', l: 'Коэффициент шума' },
2308
- { v: '—', l: 'Структурированность' },
2309
- { v: '—', l: 'Нет покрытия' },
2310
- { v: '—', l: 'Шумных паттернов' },
2311
- ];
2312
- }
2198
+ const serviceSources = src.filter(m => m.type === 'service' || m.type === 'util' || m.type === 'other');
2199
+ const errorSignal = Object.values(D.testErrors || {}).reduce((acc, v) => acc + (v?.failed || 0), 0);
2200
+ const totalBytes = src.reduce((acc, m) => acc + (m.size || 0), 0);
2201
+ const avgKb = src.length ? Math.round(totalBytes / src.length / 1024) : 0;
2202
+ const noiseRatio = src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0;
2203
+ const missingStructured = serviceSources.filter(m => !m.hasTests).length;
2204
+ items = [
2205
+ { v: serviceSources.length, l: 'Источники логов' },
2206
+ { v: avgKb + ' KB', l: 'Средний объём/файл' },
2207
+ { v: noiseRatio + '%', l: 'Коэффициент шума' },
2208
+ { v: errorSignal, l: 'Сигнал ошибок', c: errorSignal ? '#f85149' : undefined },
2209
+ { v: missingStructured, l: 'Не хватает полей', c: missingStructured ? '#e3b341' : undefined },
2210
+ ];
2313
2211
  } else if (D.hasConfig && D.features) {
2314
2212
  const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
2315
2213
  items = [
@@ -2428,219 +2326,57 @@ function renderQaOnboarding() {
2428
2326
  }
2429
2327
 
2430
2328
  function renderObservability(c) {
2431
- const o = D.observability;
2432
- if (!o) {
2433
- c.innerHTML = `<div class="onboarding-block"><h3>Наблюдаемость</h3><p>Данные анализа логов недоступны. Пересканируй проект.</p></div>`;
2434
- return;
2435
- }
2436
-
2437
- const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
2438
- const structPct = Math.round(o.metrics.structured_completeness * 100);
2439
- const actionPct = Math.round(o.metrics.error_actionability * 100);
2440
- const coveragePct = Math.round(o.metrics.coverage_of_key_flows * 100);
2441
-
2442
- function metricColor(val, goodThreshold, warnThreshold, invert) {
2443
- // invert=true: higher is worse (noise), invert=false: higher is better (coverage)
2444
- if (invert) return val > warnThreshold ? 'var(--red)' : val > goodThreshold ? 'var(--yellow)' : 'var(--green)';
2445
- return val < warnThreshold ? 'var(--red)' : val < goodThreshold ? 'var(--yellow)' : 'var(--green)';
2446
- }
2447
-
2448
- const formatLabels = { structured: 'structured', mixed: 'mixed', unstructured: 'unstructured' };
2449
- const sourceByFormat = [
2450
- { label: 'Structured', count: o.catalog.filter(c => c.format === 'structured').length, color: 'var(--green)' },
2451
- { label: 'Mixed', count: o.catalog.filter(c => c.format === 'mixed').length, color: 'var(--yellow)' },
2452
- { label: 'Unstructured', count: o.catalog.filter(c => c.format === 'unstructured').length, color: 'var(--red)' },
2329
+ const src = D.modules.filter(m => m.type !== 'test');
2330
+ const sourceGroups = [
2331
+ { label: 'Сервисы', count: src.filter(m => m.type === 'service').length },
2332
+ { label: 'Утилиты', count: src.filter(m => m.type === 'util').length },
2333
+ { label: 'Прочее', count: src.filter(m => m.type === 'other').length },
2453
2334
  ].filter(x => x.count > 0);
2454
-
2455
- const recLabels = {
2456
- 'suppress': 'убрать',
2457
- 'downgrade level': 'понизить уровень',
2458
- 'enrich fields': 'обогатить поля',
2459
- 'add event': 'добавить событие',
2460
- };
2461
- const hasAgent = !!D.agent;
2462
-
2463
- // Store catalog for buttons to reference by index (avoids inline JSON in onclick)
2464
- window.__obsCatalog = o.catalog;
2465
-
2466
- const noisyRows = o.topNoisyPatterns.slice(0, 8).map(i => {
2467
- const safePattern = escapeHtml(i.pattern).replace(/'/g, '&#39;');
2468
- const btn = hasAgent
2469
- ? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-suppress-pattern',null,null,null,{pattern:'${safePattern}',recommendation:'${i.recommendation}'})">убрать</button>`
2470
- : '';
2471
- return `
2472
- <div class="obs-list-item">
2473
- <span class="obs-priority-${i.priority}" style="flex-shrink:0">[${i.priority}]</span>
2474
- <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(i.pattern)}">${escapeHtml(i.pattern)}</span>
2475
- <strong style="flex-shrink:0;color:var(--red)">×${i.count} → ${recLabels[i.recommendation] || i.recommendation}</strong>
2476
- ${btn}
2477
- </div>`;
2478
- }).join('') || '<div class="obs-sub">Шумных паттернов не обнаружено</div>';
2479
-
2480
- const missingRows = o.missingCriticalLogs.slice(0, 8).map(i => {
2481
- const modulePath = i.pattern.split(':')[0].trim();
2482
- const safePath = escapeHtml(modulePath).replace(/'/g, '&#39;');
2483
- const btn = hasAgent
2484
- ? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-add-critical-logs',null,null,null,{modulePath:'${safePath}'})">добавить</button>`
2485
- : '';
2486
- return `
2487
- <div class="obs-list-item">
2488
- <span class="obs-priority-${i.priority}" style="flex-shrink:0">[${i.priority}]</span>
2489
- <span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(i.pattern)}">${escapeHtml(i.pattern)}</span>
2490
- <strong style="flex-shrink:0;color:var(--yellow)">${recLabels[i.recommendation] || i.recommendation}</strong>
2491
- ${btn}
2492
- </div>`;
2493
- }).join('') || '<div class="obs-sub" style="color:var(--green)">Критичные сценарии покрыты</div>';
2494
-
2495
- const fieldGaps = o.fieldGaps || {};
2496
- const fieldGapEntries = Object.entries(fieldGaps).filter(([,v]) => v > 0).sort((a,b) => b[1] - a[1]);
2497
- const fieldGapRows = fieldGapEntries.map(([name, count]) => {
2498
- const groupId = 'field-' + name.replace(/[^a-z0-9]/gi, '_');
2499
- const affectedItems = o.catalog.filter(c => (c.missingFields||[]).includes(name));
2500
- const expandBtn = hasAgent ? `<button class="obs-expand-btn" onclick="event.stopPropagation();toggleObsDetail('${groupId}')">▼</button>` : '';
2501
- const detailItems = affectedItems.map(ci => {
2502
- const catIdx = o.catalog.indexOf(ci);
2503
- 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>`;
2504
- }).join('');
2505
- const detail = hasAgent ? `
2506
- <div id="obs-detail-${groupId}" class="obs-detail">
2507
- <div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
2508
- <button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
2509
- </div>
2510
- <div class="obs-detail-list">${detailItems}</div>
2511
- <div class="obs-detail-bar">
2512
- <button class="obs-run-selected" disabled onclick="obsRunSelected('${groupId}','obs-fix-selected',{fieldName:'${escapeHtml(name)}'})">обогатить выбранные</button>
2513
- </div>
2514
- </div>` : '';
2515
- return `<div>
2516
- <div class="obs-list-item">
2517
- <span><code>${escapeHtml(name)}</code></span>
2518
- <strong style="color:${count > 20 ? 'var(--red)' : count > 5 ? 'var(--yellow)' : 'var(--muted)'}">${count} пропусков</strong>
2519
- ${expandBtn}
2520
- </div>
2521
- ${detail}
2522
- </div>`;
2523
- }).join('') || '<div class="obs-sub" style="color:var(--green)">Все обязательные поля на месте</div>';
2524
-
2525
- const catalogRows = o.catalog.slice(0, 15).map((i, idx) => {
2526
- const missing = (i.missingFields || []);
2527
- const missingStr = missing.length ? missing.join(', ') : '—';
2528
- const btn = hasAgent
2529
- ? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-fix-module',null,null,null,{catalogIndex:${idx}})">исправить</button>`
2530
- : '';
2531
- return `
2532
- <div class="obs-cat-row">
2533
- <span title="${escapeHtml(i.modulePath)}">${escapeHtml(i.modulePath.split('/').slice(-2).join('/'))}</span>
2534
- <span>${i.level}</span>
2535
- <span style="color:${i.format==='structured'?'var(--green)':i.format==='mixed'?'var(--yellow)':'var(--red)'}">${i.format}</span>
2536
- <span style="color:${missing.length > 4 ? 'var(--red)' : missing.length > 0 ? 'var(--yellow)' : 'var(--green)'}">${missing.length}/8</span>
2537
- <span title="${escapeHtml(missingStr)}" style="overflow:hidden;text-overflow:ellipsis;white-space:nowrap">${escapeHtml(missingStr)}</span>
2538
- <span style="color:${i.recommendation==='suppress'?'var(--red)':i.recommendation==='add event'?'var(--yellow)':'var(--muted)'}">${recLabels[i.recommendation] || i.recommendation}</span>
2539
- ${btn}
2540
- </div>`}).join('');
2335
+ const errorSignal = Object.values(D.testErrors || {}).reduce((acc, v) => acc + (v?.failed || 0), 0);
2336
+ const missingStructured = src.filter(m => (m.type === 'service' || m.type === 'util') && !m.hasTests).slice(0, 8);
2337
+ const noisy = [...src]
2338
+ .sort((a, b) => (b.size || 0) - (a.size || 0))
2339
+ .slice(0, 5)
2340
+ .map(m => ({ name: m.name, rel: m.relativePath, kb: Math.round((m.size || 0) / 1024) }));
2541
2341
 
2542
2342
  c.innerHTML = `
2543
2343
  <div class="onboarding-block">
2544
2344
  <h3>Наблюдаемость: что это?</h3>
2545
- <p>Аудит покрытия логами: что добавить, что убрать, что обогатить на основе статического анализа лог-вызовов.</p>
2345
+ <p>Экран для контроля отношения сигнал/шум: откуда идут логи, где растёт объём, сколько шума и какие поля/сигналы нужно структурировать в первую очередь.</p>
2546
2346
  </div>
2547
2347
 
2548
- <div class="obs-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
2348
+ <div class="obs-grid">
2549
2349
  <div class="obs-card">
2550
- <div class="obs-title">Коэффициент шума</div>
2551
- <div class="obs-value" style="color:${metricColor(noiseRatio,10,30,true)}">${noiseRatio}%</div>
2552
- <div class="obs-sub">Доля шумных лог-вызовов из всех.</div>
2553
- </div>
2554
- <div class="obs-card">
2555
- <div class="obs-title">Структурированность</div>
2556
- <div class="obs-value" style="color:${metricColor(structPct,80,50,false)}">${structPct}%</div>
2557
- <div class="obs-sub">Логи с обязательными полями (module, event, traceId).</div>
2558
- </div>
2559
- <div class="obs-card">
2560
- <div class="obs-title">Actionable ошибки</div>
2561
- <div class="obs-value" style="color:${metricColor(actionPct,80,50,false)}">${actionPct}%</div>
2562
- <div class="obs-sub">ERROR-логи с контекстом для диагностики.</div>
2563
- </div>
2564
- <div class="obs-card">
2565
- <div class="obs-title">Покрытие сценариев</div>
2566
- <div class="obs-value" style="color:${metricColor(coveragePct,80,50,false)}">${coveragePct}%</div>
2567
- <div class="obs-sub">Модули с хотя бы одним warn/error событием.</div>
2568
- </div>
2569
- </div>
2570
-
2571
- <div class="obs-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:12px">
2572
- <div class="obs-card">
2573
- <div class="obs-title">Источники по формату</div>
2350
+ <div class="obs-title">Источники логов</div>
2574
2351
  <div class="obs-list">
2575
- ${sourceByFormat.map(g => `
2576
- <div class="obs-list-item">
2577
- <span style="color:${g.color}">${g.label}</span>
2578
- <strong>${g.count}</strong>
2579
- </div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
2352
+ ${sourceGroups.map(g => `<div class="obs-list-item"><span>${g.label}</span><strong>${g.count}</strong></div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
2580
2353
  </div>
2581
2354
  </div>
2355
+
2582
2356
  <div class="obs-card">
2583
- <div class="obs-title">Классификация логов</div>
2357
+ <div class="obs-title">Объём логов (топ файлов)</div>
2584
2358
  <div class="obs-list">
2585
- <div class="obs-list-item"><span style="color:var(--red)">Мусор (убрать)</span><strong>${o.classification.trash}</strong></div>
2586
- <div class="obs-list-item"><span style="color:var(--green)">Полезные</span><strong>${o.classification.useful}</strong></div>
2587
- <div class="obs-list-item"><span style="color:var(--blue)">Критичные</span><strong>${o.classification.critical}</strong></div>
2588
- </div>
2589
- </div>
2590
- <div class="obs-card">
2591
- <div class="obs-title">Рекомендации</div>
2592
- <div class="obs-list" style="gap:2px">
2593
- ${['suppress','enrich fields','add event','downgrade level'].map(rec => {
2594
- const items = o.catalog.filter(c => c.recommendation === rec);
2595
- if (!items.length) return '';
2596
- const groupId = 'rec-' + rec.replace(/\s+/g, '-');
2597
- const expandBtn = hasAgent ? `<button class="obs-expand-btn" onclick="event.stopPropagation();toggleObsDetail('${groupId}')">▼</button>` : '';
2598
- const detailItems = items.map((ci, i) => {
2599
- const catIdx = o.catalog.indexOf(ci);
2600
- const mf = (ci.missingFields||[]).join(', ') || '—';
2601
- 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>`;
2602
- }).join('');
2603
- const detail = hasAgent ? `
2604
- <div id="obs-detail-${groupId}" class="obs-detail">
2605
- <div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
2606
- <button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
2607
- </div>
2608
- <div class="obs-detail-list">${detailItems}</div>
2609
- <div class="obs-detail-bar">
2610
- <button class="obs-run-selected" disabled onclick="obsRunSelected('${groupId}','obs-fix-selected',{recommendationType:'${rec}'})">исправить выбранные</button>
2611
- </div>
2612
- </div>` : '';
2613
- return `<div>
2614
- <div class="obs-list-item"><span>${recLabels[rec]}</span><strong>${items.length}</strong>${expandBtn}</div>
2615
- ${detail}
2616
- </div>`;
2617
- }).join('')}
2359
+ ${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>'}
2618
2360
  </div>
2619
2361
  </div>
2620
- </div>
2621
2362
 
2622
- <div class="obs-grid" style="grid-template-columns:1fr 1fr;margin-bottom:12px">
2623
2363
  <div class="obs-card">
2624
- <div class="obs-title">Что убрать — шумные паттерны</div>
2625
- <div class="obs-list" style="gap:4px">${noisyRows}</div>
2364
+ <div class="obs-title">Коэффициент шума</div>
2365
+ <div class="obs-value">${src.length ? Math.round((src.filter(m => !m.hasTests).length / src.length) * 100) : 0}%</div>
2366
+ <div class="obs-sub">Доля источников без тестового контроля (прокси-метрика шумного логирования).</div>
2626
2367
  </div>
2368
+
2627
2369
  <div class="obs-card">
2628
- <div class="obs-title">Что добавить — нет критичных логов</div>
2629
- <div class="obs-list" style="gap:4px">${missingRows}</div>
2370
+ <div class="obs-title">Сигнал ошибок</div>
2371
+ <div class="obs-value" style="color:${errorSignal ? 'var(--red)' : 'var(--green)'}">${errorSignal}</div>
2372
+ <div class="obs-sub">Сумма упавших тестов из последнего прогона как индикатор нестабильности сигналов.</div>
2630
2373
  </div>
2631
- </div>
2632
2374
 
2633
- <div class="obs-card" style="margin-bottom:12px">
2634
- <div class="obs-title">Что обогатить пробелы по полям</div>
2635
- <div class="obs-sub" style="margin-bottom:8px">Обязательные поля по стандарту: service, env, trace_id, request_id, event_name, outcome, error_code (warn/error), user_id.</div>
2636
- <div class="obs-list">${fieldGapRows}</div>
2637
- </div>
2638
-
2639
- <div class="obs-card" style="margin-bottom:12px">
2640
- <div class="obs-title">Каталог источников логов (топ 15)</div>
2641
- <div class="obs-catalog" style="margin-top:8px;border:none;padding:0">
2642
- <div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>пробелы</span><span>не хватает</span><span>действие</span><span></span></div>
2643
- ${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
2375
+ <div class="obs-card" style="grid-column:1 / -1">
2376
+ <div class="obs-title">Недостающие структурированные поля (кандидаты)</div>
2377
+ <div class="obs-list">
2378
+ ${missingStructured.map(m => `<div class="obs-list-item"><span>${m.relativePath}</span><strong>нужна схема</strong></div>`).join('') || '<div class="obs-sub">Явных кандидатов не найдено</div>'}
2379
+ </div>
2644
2380
  </div>
2645
2381
  </div>`;
2646
2382
  }
@@ -3326,7 +3062,7 @@ function renderUnmappedDetail(c) {
3326
3062
  <div style="padding:0 0 12px;display:flex;gap:8px;flex-wrap:wrap">
3327
3063
  ${D.agent ? `<button id="runAgentUnmapped" style="
3328
3064
  padding:7px 14px; background:var(--blue); border:none;
3329
- border-radius:6px; color:var(--bg); font-size:12px; font-weight:700; cursor:pointer;
3065
+ border-radius:6px; color:#000; font-size:12px; font-weight:700; cursor:pointer;
3330
3066
  ">▶ Разобрать через ${D.agent === 'claude' ? 'Claude Code' : 'Codex'}</button>` : ''}
3331
3067
  <button id="copyUnmappedDrill" style="
3332
3068
  padding:7px 14px; background:var(--bg-card); border:1px solid var(--border);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.70",
3
+ "version": "0.3.72",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {