viberadar 0.3.76 → 0.3.78

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.
@@ -670,8 +670,8 @@
670
670
  }
671
671
  .file-row-fix-btn:hover { opacity: 0.85; }
672
672
  .obs-action-btn {
673
- display: inline-flex; align-items: center; gap: 3px;
674
- padding: 2px 7px; font-size: 10px;
673
+ display: inline-flex; align-items: center; gap: 4px;
674
+ padding: 6px 14px; font-size: 12px; min-height: 32px;
675
675
  background: transparent; border: 1px solid var(--border); border-radius: 4px;
676
676
  color: var(--dim); cursor: pointer; white-space: nowrap; flex-shrink: 0;
677
677
  transition: background 0.1s, color 0.1s, border-color 0.1s;
@@ -679,30 +679,114 @@
679
679
  .obs-action-btn:hover { background: var(--accent); color: var(--bg); border-color: var(--accent); }
680
680
  .obs-batch-btn { border-color: var(--yellow); color: var(--yellow); }
681
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; }
682
+ .obs-expand-btn { background:none; border:none; color:var(--muted); cursor:pointer; font-size:12px; padding:5px 8px; }
683
683
  .obs-expand-btn:hover { color:var(--accent); }
684
684
  .obs-detail { display:none; padding:6px 0 2px 0; border-top:1px dashed var(--border); margin-top:4px; }
685
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); }
686
+ .obs-detail-list { max-height:400px; overflow-y:auto; display:flex; flex-direction:column; gap:2px; }
687
+ .obs-detail-item { display:flex; align-items:center; gap:6px; font-size:12px; color:var(--muted); padding:6px 4px; }
688
+ .obs-detail-item input[type="checkbox"] { margin:0; flex-shrink:0; accent-color:var(--accent); width:18px; height:18px; }
689
689
  .obs-detail-item span { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
690
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; }
691
+ .obs-run-selected { padding:8px 16px; font-size:13px; font-weight:600; background:var(--accent); color:var(--bg); border:none; border-radius:4px; cursor:pointer; min-height:36px; }
692
692
  .obs-run-selected:hover { opacity:0.85; }
693
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; }
694
+ .obs-select-all { font-size:12px; color:var(--dim); cursor:pointer; background:none; border:none; padding:4px 8px; }
695
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; }
696
+ .obs-tier-badge { display:inline-block; padding:3px 8px; border-radius:3px; font-size:11px; font-weight:700; letter-spacing:0.5px; }
697
697
  .obs-tier-critical { background:rgba(248,81,73,0.2); color:var(--red); }
698
698
  .obs-tier-important { background:rgba(227,179,65,0.2); color:var(--yellow); }
699
699
  .obs-tier-normal { background:rgba(139,148,158,0.15); color:var(--muted); }
700
700
  .obs-fp-list { font-size:11px; color:var(--muted); margin:4px 0 0 18px; }
701
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; }
702
+ .obs-fp-type { color:var(--yellow); font-weight:600; white-space:nowrap; font-size:11px; }
703
703
  .obs-fp-line { color:var(--dim); font-size:10px; flex-shrink:0; }
704
704
  .obs-tier-group { margin-bottom:8px; }
705
705
  .obs-tier-group-header { display:flex; align-items:center; gap:8px; padding:4px 0; font-size:12px; font-weight:600; color:var(--text); }
706
+
707
+ /* ── Observability tabs UX ── */
708
+ .obs-kpi-strip {
709
+ position:sticky; top:0; z-index:5;
710
+ display:grid; grid-template-columns:repeat(4,1fr); gap:8px;
711
+ background:var(--bg); padding:10px 0 12px 0; border-bottom:1px solid var(--border); margin-bottom:0;
712
+ }
713
+ .obs-kpi {
714
+ display:flex; align-items:baseline; gap:8px; padding:6px 10px;
715
+ background:var(--bg-card); border:1px solid var(--border); border-radius:6px;
716
+ }
717
+ .obs-kpi-value { font-size:18px; font-weight:700; font-variant-numeric:tabular-nums; }
718
+ .obs-kpi-label { font-size:11px; color:var(--muted); white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }
719
+ .obs-tabs {
720
+ position:sticky; top:52px; z-index:4;
721
+ display:flex; background:var(--bg); border-radius:6px; padding:3px; gap:2px; margin:12px 0 16px 0;
722
+ }
723
+ .obs-tab {
724
+ flex:1; padding:8px 12px; text-align:center; font-size:12px; font-weight:600;
725
+ border-radius:4px; cursor:pointer; color:var(--muted); user-select:none;
726
+ transition:background 0.15s, color 0.15s; white-space:nowrap;
727
+ }
728
+ .obs-tab:hover { color:var(--text); background:var(--bg-hover); }
729
+ .obs-tab.active { background:var(--bg-card); color:var(--text); }
730
+ .obs-tab-badge {
731
+ display:inline-flex; align-items:center; justify-content:center;
732
+ min-width:18px; height:18px; padding:0 5px; border-radius:9px;
733
+ font-size:10px; font-weight:700; margin-left:6px; vertical-align:middle;
734
+ }
735
+ .obs-tab-badge.red { background:rgba(248,81,73,0.2); color:var(--red); }
736
+ .obs-tab-badge.yellow { background:rgba(227,179,65,0.2); color:var(--yellow); }
737
+ .obs-tab-badge.muted { background:rgba(139,148,158,0.15); color:var(--muted); }
738
+ .obs-tab-content { display:none; }
739
+ .obs-tab-content.active { display:block; }
740
+ @media (max-width:640px) { .obs-kpi-strip { grid-template-columns:repeat(2,1fr); } }
741
+
742
+ /* ── Tab hint panels ── */
743
+ .obs-hint {
744
+ margin-bottom: 12px;
745
+ border: 1px solid var(--border);
746
+ border-radius: 8px;
747
+ overflow: hidden;
748
+ }
749
+ .obs-hint-toggle {
750
+ display: flex; align-items: center; gap: 8px;
751
+ width: 100%; padding: 10px 14px;
752
+ background: none; border: none; cursor: pointer;
753
+ color: var(--muted); font-size: 12px; font-weight: 600;
754
+ text-align: left; transition: background 0.1s, color 0.1s;
755
+ }
756
+ .obs-hint-toggle:hover { background: var(--bg-card); color: var(--text); }
757
+ .obs-hint-icon {
758
+ display: inline-flex; align-items: center; justify-content: center;
759
+ width: 18px; height: 18px; border-radius: 50%;
760
+ border: 1.5px solid currentColor; font-size: 11px; font-weight: 700;
761
+ flex-shrink: 0;
762
+ }
763
+ .obs-hint-chevron { margin-left: auto; font-size: 10px; transition: transform 0.2s; }
764
+ .obs-hint.open .obs-hint-chevron { transform: rotate(180deg); }
765
+ .obs-hint-body {
766
+ display: none;
767
+ padding: 0 16px 16px 16px;
768
+ background: var(--bg-card);
769
+ font-size: 12px; color: var(--muted); line-height: 1.6;
770
+ }
771
+ .obs-hint.open .obs-hint-body { display: block; }
772
+ .obs-hint-section { margin-top: 12px; }
773
+ .obs-hint-section-title { font-size: 11px; font-weight: 700; color: var(--text); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
774
+ .obs-hint-code {
775
+ font-family: monospace; font-size: 11px;
776
+ background: var(--bg); border: 1px solid var(--border);
777
+ border-radius: 5px; padding: 8px 10px; margin: 4px 0;
778
+ line-height: 1.5; overflow-x: auto; white-space: pre;
779
+ }
780
+ .obs-hint-code .hl-red { color: var(--red); }
781
+ .obs-hint-code .hl-green { color: var(--green); }
782
+ .obs-hint-code .hl-yellow { color: var(--yellow); }
783
+ .obs-hint-code .hl-dim { color: var(--dim); }
784
+ .obs-hint-row { display: flex; gap: 12px; }
785
+ .obs-hint-row > * { flex: 1; min-width: 0; }
786
+ .obs-hint-label { font-size: 10px; font-weight: 700; margin-bottom: 3px; }
787
+ .obs-hint-label.before { color: var(--red); }
788
+ .obs-hint-label.after { color: var(--green); }
789
+
706
790
  .file-row-err-badge {
707
791
  display: inline-flex; align-items: center;
708
792
  font-size: 11px; padding: 1px 6px; border-radius: 10px;
@@ -1182,6 +1266,21 @@ let fileRowsRenderKey = '';
1182
1266
  let fileRowsRenderLimit = FILE_ROWS_INITIAL_LIMIT;
1183
1267
  let e2ePlan = null; // current E2E plan object
1184
1268
  let e2ePlanLoading = false;
1269
+ let obsActiveTab = 'overview'; // active observability tab
1270
+
1271
+ function toggleObsHint(id) {
1272
+ document.getElementById(id).classList.toggle('open');
1273
+ }
1274
+
1275
+ function switchObsTab(tabId) {
1276
+ obsActiveTab = tabId;
1277
+ history.replaceState(null, '', location.pathname + location.search + '#obs-tab=' + tabId);
1278
+ document.querySelectorAll('.obs-tab').forEach(t =>
1279
+ t.classList.toggle('active', t.dataset.obstab === tabId));
1280
+ document.querySelectorAll('.obs-tab-content').forEach(p => {
1281
+ p.classList.toggle('active', p.dataset.obstab === tabId);
1282
+ });
1283
+ }
1185
1284
  const modeStore = {
1186
1285
  qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
1187
1286
  observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
@@ -2280,6 +2379,8 @@ async function init() {
2280
2379
 
2281
2380
  contextMode = getModeFromPath();
2282
2381
  view = D.hasConfig ? 'features' : 'files';
2382
+ const _obsHash = window.location.hash;
2383
+ if (_obsHash.startsWith('#obs-tab=')) obsActiveTab = _obsHash.replace('#obs-tab=', '');
2283
2384
  applyHashRoute();
2284
2385
 
2285
2386
  if (!D.hasConfig) {
@@ -2493,7 +2594,7 @@ function renderObservability(c) {
2493
2594
  // Store catalog for buttons to reference by index (avoids inline JSON in onclick)
2494
2595
  window.__obsCatalog = o.catalog;
2495
2596
 
2496
- const noisyRows = o.topNoisyPatterns.slice(0, 8).map(i => {
2597
+ const noisyRows = o.topNoisyPatterns.map(i => {
2497
2598
  const safePattern = escapeHtml(i.pattern).replace(/'/g, ''');
2498
2599
  const btn = hasAgent
2499
2600
  ? `<button class="obs-action-btn" onclick="event.stopPropagation();runAgentTask('obs-suppress-pattern',null,null,null,{pattern:'${safePattern}',recommendation:'${i.recommendation}'})">убрать</button>`
@@ -2595,7 +2696,7 @@ function renderObservability(c) {
2595
2696
  </div>`;
2596
2697
  }).join('') || '<div class="obs-sub" style="color:var(--green)">Все обязательные поля на месте</div>';
2597
2698
 
2598
- const catalogRows = o.catalog.slice(0, 15).map((i, idx) => {
2699
+ const catalogRows = o.catalog.map((i, idx) => {
2599
2700
  const missing = (i.missingFields || []);
2600
2701
  const missingStr = missing.length ? missing.join(', ') : '—';
2601
2702
  const btn = hasAgent
@@ -2612,110 +2713,326 @@ function renderObservability(c) {
2612
2713
  ${btn}
2613
2714
  </div>`}).join('');
2614
2715
 
2716
+ const noisyCount = o.topNoisyPatterns.length;
2717
+ const missingCount = v2Data.length;
2718
+ const enrichCount = fieldGapEntries.length;
2719
+ const catalogCount = o.catalog.length;
2720
+
2615
2721
  c.innerHTML = `
2616
2722
  <div class="onboarding-block">
2617
2723
  <h3>Наблюдаемость: что это?</h3>
2618
2724
  <p>Аудит покрытия логами: что добавить, что убрать, что обогатить — на основе статического анализа лог-вызовов.</p>
2619
2725
  </div>
2620
2726
 
2621
- <div class="obs-grid" style="grid-template-columns:repeat(4,1fr);margin-bottom:12px">
2622
- <div class="obs-card">
2623
- <div class="obs-title">Коэффициент шума</div>
2624
- <div class="obs-value" style="color:${metricColor(noiseRatio,10,30,true)}">${noiseRatio}%</div>
2625
- <div class="obs-sub">Доля шумных лог-вызовов из всех.</div>
2727
+ <div class="obs-kpi-strip">
2728
+ <div class="obs-kpi">
2729
+ <span class="obs-kpi-value" style="color:${metricColor(noiseRatio,10,30,true)}">${noiseRatio}%</span>
2730
+ <span class="obs-kpi-label">Шум</span>
2626
2731
  </div>
2627
- <div class="obs-card">
2628
- <div class="obs-title">Структурированность</div>
2629
- <div class="obs-value" style="color:${metricColor(structPct,80,50,false)}">${structPct}%</div>
2630
- <div class="obs-sub">Логи с обязательными полями (module, event, traceId).</div>
2732
+ <div class="obs-kpi">
2733
+ <span class="obs-kpi-value" style="color:${metricColor(structPct,80,50,false)}">${structPct}%</span>
2734
+ <span class="obs-kpi-label">Структурированность</span>
2631
2735
  </div>
2632
- <div class="obs-card">
2633
- <div class="obs-title">Actionable ошибки</div>
2634
- <div class="obs-value" style="color:${metricColor(actionPct,80,50,false)}">${actionPct}%</div>
2635
- <div class="obs-sub">ERROR-логи с контекстом для диагностики.</div>
2736
+ <div class="obs-kpi">
2737
+ <span class="obs-kpi-value" style="color:${metricColor(actionPct,80,50,false)}">${actionPct}%</span>
2738
+ <span class="obs-kpi-label">Actionable ошибки</span>
2636
2739
  </div>
2637
- <div class="obs-card">
2638
- <div class="obs-title">Покрытие сценариев</div>
2639
- <div class="obs-value" style="color:${metricColor(coveragePct,80,50,false)}">${coveragePct}%</div>
2640
- <div class="obs-sub">Модули с хотя бы одним warn/error событием.</div>
2740
+ <div class="obs-kpi">
2741
+ <span class="obs-kpi-value" style="color:${metricColor(coveragePct,80,50,false)}">${coveragePct}%</span>
2742
+ <span class="obs-kpi-label">Покрытие сценариев</span>
2641
2743
  </div>
2642
2744
  </div>
2643
2745
 
2644
- <div class="obs-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:12px">
2645
- <div class="obs-card">
2646
- <div class="obs-title">Источники по формату</div>
2647
- <div class="obs-list">
2648
- ${sourceByFormat.map(g => `
2649
- <div class="obs-list-item">
2650
- <span style="color:${g.color}">${g.label}</span>
2651
- <strong>${g.count}</strong>
2652
- </div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
2746
+ <div class="obs-tabs">
2747
+ <div class="obs-tab" data-obstab="overview">Обзор</div>
2748
+ <div class="obs-tab" data-obstab="remove">Убрать${noisyCount ? `<span class="obs-tab-badge red">${noisyCount}</span>` : ''}</div>
2749
+ <div class="obs-tab" data-obstab="add">Добавить${missingCount ? `<span class="obs-tab-badge yellow">${missingCount}</span>` : ''}</div>
2750
+ <div class="obs-tab" data-obstab="enrich">Обогатить${enrichCount ? `<span class="obs-tab-badge yellow">${enrichCount}</span>` : ''}</div>
2751
+ <div class="obs-tab" data-obstab="catalog">Каталог${catalogCount ? `<span class="obs-tab-badge muted">${catalogCount}</span>` : ''}</div>
2752
+ </div>
2753
+
2754
+ <div class="obs-tab-content" data-obstab="overview">
2755
+ <div class="obs-hint" id="obs-hint-overview">
2756
+ <button class="obs-hint-toggle" onclick="toggleObsHint('obs-hint-overview')">
2757
+ <span class="obs-hint-icon">?</span>
2758
+ Что такое «Обзор» и как читать эти числа?
2759
+ <span class="obs-hint-chevron">▼</span>
2760
+ </button>
2761
+ <div class="obs-hint-body">
2762
+ <p>Обзор — это «пульс» логирования в проекте. Четыре метрики наверху показывают, насколько логи полезны прямо сейчас.</p>
2763
+ <div class="obs-hint-section">
2764
+ <div class="obs-hint-section-title">Что означает каждая метрика</div>
2765
+ <div class="obs-hint-code"><span class="hl-red">Шум 40%</span> — почти половина логов бесполезны. Они засоряют Kibana/Datadog
2766
+ и скрывают реальные ошибки.
2767
+
2768
+ <span class="hl-yellow">Структурированность 20%</span> — большинство логов — просто строки вроде
2769
+ <span class="hl-dim">"Starting process..."</span>. Их нельзя фильтровать, группировать,
2770
+ строить алерты. Цель — 80%+.
2771
+
2772
+ <span class="hl-yellow">Actionable ошибки 50%</span> — половина ошибок логируется без контекста.
2773
+ Инженер видит <span class="hl-dim">"Error occurred"</span> — и ничего больше.
2774
+
2775
+ <span class="hl-red">Покрытие сценариев 24%</span> — 76% модулей вообще не имеют warn/error логов.
2776
+ Когда они сломаются — узнаешь только от пользователей.</div>
2777
+ </div>
2778
+ <div class="obs-hint-section">
2779
+ <div class="obs-hint-section-title">Блок «Рекомендации»</div>
2780
+ <p>Итоговый план действий по всему проекту: сколько источников нужно <span style="color:var(--red)">убрать</span>, <span style="color:var(--yellow)">обогатить</span>, <span style="color:var(--yellow)">добавить</span>. Каждая строчка раскрывается — можно выбрать файлы и отдать агенту.</p>
2781
+ </div>
2653
2782
  </div>
2654
2783
  </div>
2655
- <div class="obs-card">
2656
- <div class="obs-title">Классификация логов</div>
2657
- <div class="obs-list">
2658
- <div class="obs-list-item"><span style="color:var(--red)">Мусор (убрать)</span><strong>${o.classification.trash}</strong></div>
2659
- <div class="obs-list-item"><span style="color:var(--green)">Полезные</span><strong>${o.classification.useful}</strong></div>
2660
- <div class="obs-list-item"><span style="color:var(--blue)">Критичные</span><strong>${o.classification.critical}</strong></div>
2784
+ <div class="obs-grid" style="grid-template-columns:1fr 1fr 1fr;margin-bottom:12px">
2785
+ <div class="obs-card">
2786
+ <div class="obs-title">Источники по формату</div>
2787
+ <div class="obs-list">
2788
+ ${sourceByFormat.map(g => `
2789
+ <div class="obs-list-item">
2790
+ <span style="color:${g.color}">${g.label}</span>
2791
+ <strong>${g.count}</strong>
2792
+ </div>`).join('') || '<div class="obs-sub">Нет данных</div>'}
2793
+ </div>
2661
2794
  </div>
2662
- </div>
2663
- <div class="obs-card">
2664
- <div class="obs-title">Рекомендации</div>
2665
- <div class="obs-list" style="gap:2px">
2666
- ${['suppress','enrich fields','add event','downgrade level'].map(rec => {
2667
- const items = o.catalog.filter(c => c.recommendation === rec);
2668
- if (!items.length) return '';
2669
- const groupId = 'rec-' + rec.replace(/\s+/g, '-');
2670
- const expandBtn = hasAgent ? `<button class="obs-expand-btn" onclick="event.stopPropagation();toggleObsDetail('${groupId}')">▼</button>` : '';
2671
- const detailItems = items.map((ci, i) => {
2672
- const catIdx = o.catalog.indexOf(ci);
2673
- const mf = (ci.missingFields||[]).join(', ') || '—';
2674
- 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>`;
2675
- }).join('');
2676
- const detail = hasAgent ? `
2677
- <div id="obs-detail-${groupId}" class="obs-detail">
2678
- <div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
2679
- <button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
2680
- </div>
2681
- <div class="obs-detail-list">${detailItems}</div>
2682
- <div class="obs-detail-bar">
2683
- <button class="obs-run-selected" disabled onclick="obsRunSelected('${groupId}','obs-fix-selected',{recommendationType:'${rec}'})">исправить выбранные</button>
2684
- </div>
2685
- </div>` : '';
2686
- return `<div>
2687
- <div class="obs-list-item"><span>${recLabels[rec]}</span><strong>${items.length}</strong>${expandBtn}</div>
2688
- ${detail}
2689
- </div>`;
2690
- }).join('')}
2795
+ <div class="obs-card">
2796
+ <div class="obs-title">Классификация логов</div>
2797
+ <div class="obs-list">
2798
+ <div class="obs-list-item"><span style="color:var(--red)">Мусор (убрать)</span><strong>${o.classification.trash}</strong></div>
2799
+ <div class="obs-list-item"><span style="color:var(--green)">Полезные</span><strong>${o.classification.useful}</strong></div>
2800
+ <div class="obs-list-item"><span style="color:var(--blue)">Критичные</span><strong>${o.classification.critical}</strong></div>
2801
+ </div>
2802
+ </div>
2803
+ <div class="obs-card">
2804
+ <div class="obs-title">Рекомендации</div>
2805
+ <div class="obs-list" style="gap:2px">
2806
+ ${['suppress','enrich fields','add event','downgrade level'].map(rec => {
2807
+ const items = o.catalog.filter(c => c.recommendation === rec);
2808
+ if (!items.length) return '';
2809
+ const groupId = 'rec-' + rec.replace(/\s+/g, '-');
2810
+ const expandBtn = hasAgent ? `<button class="obs-expand-btn" onclick="event.stopPropagation();toggleObsDetail('${groupId}')">▼</button>` : '';
2811
+ const detailItems = items.map((ci, i) => {
2812
+ const catIdx = o.catalog.indexOf(ci);
2813
+ const mf = (ci.missingFields||[]).join(', ') || '—';
2814
+ 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>`;
2815
+ }).join('');
2816
+ const detail = hasAgent ? `
2817
+ <div id="obs-detail-${groupId}" class="obs-detail">
2818
+ <div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
2819
+ <button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
2820
+ </div>
2821
+ <div class="obs-detail-list">${detailItems}</div>
2822
+ <div class="obs-detail-bar">
2823
+ <button class="obs-run-selected" disabled onclick="obsRunSelected('${groupId}','obs-fix-selected',{recommendationType:'${rec}'})">исправить выбранные</button>
2824
+ </div>
2825
+ </div>` : '';
2826
+ return `<div>
2827
+ <div class="obs-list-item"><span>${recLabels[rec]}</span><strong>${items.length}</strong>${expandBtn}</div>
2828
+ ${detail}
2829
+ </div>`;
2830
+ }).join('')}
2831
+ </div>
2691
2832
  </div>
2692
2833
  </div>
2693
2834
  </div>
2694
2835
 
2695
- <div class="obs-grid" style="grid-template-columns:1fr 1fr;margin-bottom:12px">
2836
+ <div class="obs-tab-content" data-obstab="remove">
2837
+ <div class="obs-hint" id="obs-hint-remove">
2838
+ <button class="obs-hint-toggle" onclick="toggleObsHint('obs-hint-remove')">
2839
+ <span class="obs-hint-icon">?</span>
2840
+ Зачем убирать эти логи? Разве лишний лог — не лучше, чем ничего?
2841
+ <span class="obs-hint-chevron">▼</span>
2842
+ </button>
2843
+ <div class="obs-hint-body">
2844
+ <p>Шумные логи — это строки, которые пишутся при каждом запросе или в цикле и не несут полезной информации. Они создают три реальные проблемы:</p>
2845
+ <ul style="margin:8px 0 0 16px;display:flex;flex-direction:column;gap:4px">
2846
+ <li><strong style="color:var(--red)">Топят сигнал.</strong> Когда в Kibana 10 000 строк в минуту, реальная ошибка теряется на странице 47.</li>
2847
+ <li><strong style="color:var(--yellow)">Стоят деньги.</strong> Datadog, New Relic, Elastic — тарификация по объёму. Шум = деньги на ветер.</li>
2848
+ <li><strong style="color:var(--muted)">Замедляют дебаг.</strong> Инженер тратит 20 минут, чтобы понять, что «[unknown]» — это просто фоновый краулер.</li>
2849
+ </ul>
2850
+ <div class="obs-hint-section">
2851
+ <div class="obs-hint-section-title">До / После</div>
2852
+ <div class="obs-hint-row">
2853
+ <div>
2854
+ <div class="obs-hint-label before">БЫЛО — шум</div>
2855
+ <div class="obs-hint-code"><span class="hl-red">logger.info('[unknown]') // ×116/день</span>
2856
+ <span class="hl-red">logger.info('Converting WebM to OGG...')// ×200/день</span>
2857
+ <span class="hl-red">logger.info('Using API key from env') // каждый запрос</span></div>
2858
+ </div>
2859
+ <div>
2860
+ <div class="obs-hint-label after">СТАЛО — чисто</div>
2861
+ <div class="obs-hint-code"><span class="hl-green">// строки удалены или понижены до debug</span>
2862
+ <span class="hl-dim">// В production debug-логи выключены</span>
2863
+ <span class="hl-green">// В мониторинге только важные события</span></div>
2864
+ </div>
2865
+ </div>
2866
+ </div>
2867
+ <div class="obs-hint-section">
2868
+ <div class="obs-hint-section-title">Как использовать</div>
2869
+ <p>Нажми <strong>«убрать»</strong> рядом с паттерном — агент найдёт все места в коде и либо удалит строку, либо заменит <code>logger.info</code> на <code>logger.debug</code>. Можно обработать несколько паттернов сразу.</p>
2870
+ </div>
2871
+ </div>
2872
+ </div>
2696
2873
  <div class="obs-card">
2697
- <div class="obs-title">Что убрать — шумные паттерны</div>
2874
+ <div class="obs-title">Шумные паттерны</div>
2698
2875
  <div class="obs-list" style="gap:4px">${noisyRows}</div>
2699
2876
  </div>
2877
+ </div>
2878
+
2879
+ <div class="obs-tab-content" data-obstab="add">
2880
+ <div class="obs-hint" id="obs-hint-add">
2881
+ <button class="obs-hint-toggle" onclick="toggleObsHint('obs-hint-add')">
2882
+ <span class="obs-hint-icon">?</span>
2883
+ Почему важно добавлять логи именно в эти места?
2884
+ <span class="obs-hint-chevron">▼</span>
2885
+ </button>
2886
+ <div class="obs-hint-body">
2887
+ <p>Сканер нашёл «точки отказа» — конструкции в коде, где что-то может пойти не так, но лог туда не написан. Если здесь случится ошибка в production — ты узнаешь об этом только от пользователя.</p>
2888
+ <div class="obs-hint-section">
2889
+ <div class="obs-hint-section-title">Типы точек отказа</div>
2890
+ <div class="obs-hint-code"><span class="hl-red">empty-catch</span> — catch(){} без лога. Ошибка молча проглочена.
2891
+ <span class="hl-red">catch-no-log</span> — catch(e){ doSomething() } — ошибка обработана, но не залогирована.
2892
+ <span class="hl-yellow">http-no-error-handling</span> — HTTP-запрос без обработки ошибки (нет .catch или try/catch).
2893
+ <span class="hl-yellow">db-no-error-handling</span> — запрос к БД без обработки ошибки.
2894
+ <span class="hl-yellow">throw-no-log</span> — throw new Error(...) без предварительного лога.
2895
+ <span class="hl-dim">error-check-no-log</span> — if (err) { return } без лога.</div>
2896
+ </div>
2897
+ <div class="obs-hint-section">
2898
+ <div class="obs-hint-section-title">До / После</div>
2899
+ <div class="obs-hint-row">
2900
+ <div>
2901
+ <div class="obs-hint-label before">БЫЛО — немая ошибка</div>
2902
+ <div class="obs-hint-code"><span class="hl-dim">async function processPayment(data) {</span>
2903
+ <span class="hl-dim">try {</span>
2904
+ <span class="hl-dim">await stripe.charge(data);</span>
2905
+ <span class="hl-red">} catch (e) {</span>
2906
+ <span class="hl-red"> // пусто! ошибка проглочена</span>
2907
+ <span class="hl-red">}</span>
2908
+ <span class="hl-dim">}</span></div>
2909
+ </div>
2910
+ <div>
2911
+ <div class="obs-hint-label after">СТАЛО — видимая ошибка</div>
2912
+ <div class="obs-hint-code"><span class="hl-dim">async function processPayment(data) {</span>
2913
+ <span class="hl-dim">try {</span>
2914
+ <span class="hl-dim">await stripe.charge(data);</span>
2915
+ <span class="hl-green">} catch (e) {</span>
2916
+ <span class="hl-green"> logger.error('payment_failed', {</span>
2917
+ <span class="hl-green"> error: e.message, userId: data.userId</span>
2918
+ <span class="hl-green"> });</span>
2919
+ <span class="hl-green"> throw e;</span>
2920
+ <span class="hl-green">}</span>
2921
+ <span class="hl-dim">}</span></div>
2922
+ </div>
2923
+ </div>
2924
+ </div>
2925
+ <div class="obs-hint-section">
2926
+ <div class="obs-hint-section-title">Как использовать</div>
2927
+ <p>Модули разбиты по уровню риска: <span style="color:var(--red)">Критичные</span> — трогать в первую очередь (платежи, авторизация, запись данных). Раскрой группу, выбери модули и нажми <strong>«добавить логи выбранным»</strong>.</p>
2928
+ </div>
2929
+ </div>
2930
+ </div>
2700
2931
  <div class="obs-card">
2701
- <div class="obs-title">Что добавить — нет критичных логов</div>
2932
+ <div class="obs-title">Нет критичных логов</div>
2702
2933
  <div class="obs-list" style="gap:4px">${missingSection}</div>
2703
2934
  </div>
2704
2935
  </div>
2705
2936
 
2706
- <div class="obs-card" style="margin-bottom:12px">
2707
- <div class="obs-title">Что обогатить — пробелы по полям</div>
2708
- <div class="obs-sub" style="margin-bottom:8px">Обязательные поля по стандарту: service, env, trace_id, request_id, event_name, outcome, error_code (warn/error), user_id.</div>
2709
- <div class="obs-list">${fieldGapRows}</div>
2937
+ <div class="obs-tab-content" data-obstab="enrich">
2938
+ <div class="obs-hint" id="obs-hint-enrich">
2939
+ <button class="obs-hint-toggle" onclick="toggleObsHint('obs-hint-enrich')">
2940
+ <span class="obs-hint-icon">?</span>
2941
+ Какая разница, есть ли поля в логе? Главное, что сообщение есть.
2942
+ <span class="obs-hint-chevron">▼</span>
2943
+ </button>
2944
+ <div class="obs-hint-body">
2945
+ <p>Лог без полей — это как сигнал пожарной тревоги без адреса. Ты знаешь, что что-то горит, но не знаешь где и что именно. Структурированные поля превращают лог из строки в <em>данные</em>.</p>
2946
+ <div class="obs-hint-section">
2947
+ <div class="obs-hint-section-title">До / После</div>
2948
+ <div class="obs-hint-row">
2949
+ <div>
2950
+ <div class="obs-hint-label before">БЫЛО — бесполезный лог</div>
2951
+ <div class="obs-hint-code"><span class="hl-red">logger.error('Request failed')</span>
2952
+ <span class="hl-dim">// В Kibana: "Request failed"</span>
2953
+ <span class="hl-dim">// Что за запрос? Чей? Когда?</span>
2954
+ <span class="hl-dim">// Какая ошибка? Какой эндпоинт?</span>
2955
+ <span class="hl-dim">// → звонишь разработчику в 2 ночи</span></div>
2956
+ </div>
2957
+ <div>
2958
+ <div class="obs-hint-label after">СТАЛО — actionable лог</div>
2959
+ <div class="obs-hint-code"><span class="hl-green">logger.error('request_failed', {</span>
2960
+ <span class="hl-green">event: 'request_failed',</span>
2961
+ <span class="hl-green">service: 'payment-api',</span>
2962
+ <span class="hl-green">trace_id: req.traceId,</span>
2963
+ <span class="hl-green">user_id: req.userId,</span>
2964
+ <span class="hl-green">error_code: e.code,</span>
2965
+ <span class="hl-green">outcome: 'failure'</span>
2966
+ <span class="hl-green">})</span>
2967
+ <span class="hl-dim">// → фильтр по user_id, алерт по error_code</span></div>
2968
+ </div>
2969
+ </div>
2970
+ </div>
2971
+ <div class="obs-hint-section">
2972
+ <div class="obs-hint-section-title">Обязательные поля и зачем они нужны</div>
2973
+ <div class="obs-hint-code"><span class="hl-green">service</span> — какой сервис логирует (нужен для фильтрации в мульти-сервисе)
2974
+ <span class="hl-green">event_name</span> — машиночитаемое название события (для алертов и дашбордов)
2975
+ <span class="hl-green">trace_id</span> — сквозной ID запроса (связывает логи из разных сервисов)
2976
+ <span class="hl-green">outcome</span> — success/failure (позволяет строить SLO-метрики)
2977
+ <span class="hl-yellow">user_id</span> — чей запрос (обязателен для error/warn)
2978
+ <span class="hl-yellow">error_code</span> — код ошибки (обязателен для error/warn)</div>
2979
+ </div>
2980
+ <div class="obs-hint-section">
2981
+ <div class="obs-hint-section-title">Как использовать</div>
2982
+ <p>Каждая строчка — пропущенное поле и сколько раз оно отсутствует. Раскрой строку, выбери затронутые модули и нажми <strong>«обогатить выбранные»</strong> — агент добавит поля в существующие лог-вызовы.</p>
2983
+ </div>
2984
+ </div>
2985
+ </div>
2986
+ <div class="obs-card">
2987
+ <div class="obs-title">Пробелы по полям</div>
2988
+ <div class="obs-sub" style="margin-bottom:8px">Обязательные поля по стандарту: service, env, trace_id, request_id, event_name, outcome, error_code (warn/error), user_id.</div>
2989
+ <div class="obs-list">${fieldGapRows}</div>
2990
+ </div>
2710
2991
  </div>
2711
2992
 
2712
- <div class="obs-card" style="margin-bottom:12px">
2713
- <div class="obs-title">Каталог источников логов (топ 15)</div>
2714
- <div class="obs-catalog" style="margin-top:8px;border:none;padding:0">
2715
- <div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>пробелы</span><span>не хватает</span><span>действие</span><span></span></div>
2716
- ${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
2993
+ <div class="obs-tab-content" data-obstab="catalog">
2994
+ <div class="obs-hint" id="obs-hint-catalog">
2995
+ <button class="obs-hint-toggle" onclick="toggleObsHint('obs-hint-catalog')">
2996
+ <span class="obs-hint-icon">?</span>
2997
+ Как читать таблицу и что означают колонки?
2998
+ <span class="obs-hint-chevron">▼</span>
2999
+ </button>
3000
+ <div class="obs-hint-body">
3001
+ <p>Каталог — полная карта всех источников логов в проекте. Каждая строка = один файл/модуль. Это отправная точка для ручного аудита или приоритизации задач агента.</p>
3002
+ <div class="obs-hint-section">
3003
+ <div class="obs-hint-section-title">Что означают колонки</div>
3004
+ <div class="obs-hint-code"><span class="hl-green">формат</span> <span class="hl-green">structured</span> — логи в JSON с полями. Можно фильтровать, алертить.
3005
+ <span class="hl-yellow">mixed</span> — часть логов структурированы, часть — нет.
3006
+ <span class="hl-red">unstructured</span> — просто строки. Ни алертов, ни поиска по полям.
3007
+
3008
+ <span class="hl-yellow">пробелы</span> <span class="hl-dim">N/8</span> — сколько из 8 обязательных полей отсутствуют.
3009
+ <span class="hl-green">0/8</span> — отлично. <span class="hl-red">5/8</span> — нужно работать.
3010
+
3011
+ <span class="hl-dim">действие</span> suppress — модуль генерирует шум, логи нужно убрать/понизить.
3012
+ enrich fields — логи есть, но без нужных полей.
3013
+ add event — нет вообще никаких логов в критичных местах.
3014
+ downgrade level— используется INFO там, где достаточно DEBUG.</div>
3015
+ </div>
3016
+ <div class="obs-hint-section">
3017
+ <div class="obs-hint-section-title">Как использовать</div>
3018
+ <p>Нажми <strong>«исправить»</strong> в строке конкретного модуля — агент откроет файл, посмотрит контекст и применит рекомендованное действие. Удобно для точечных правок без выбора целой группы.</p>
3019
+ </div>
3020
+ </div>
3021
+ </div>
3022
+ <div class="obs-card">
3023
+ <div class="obs-title">Каталог источников логов</div>
3024
+ <div class="obs-catalog" style="margin-top:8px;border:none;padding:0">
3025
+ <div class="obs-cat-row head"><span>модуль</span><span>уровень</span><span>формат</span><span>пробелы</span><span>не хватает</span><span>действие</span><span></span></div>
3026
+ ${catalogRows || '<div class="obs-row">Логи не найдены</div>'}
3027
+ </div>
2717
3028
  </div>
2718
3029
  </div>`;
3030
+
3031
+ // Attach tab click handlers and restore active tab
3032
+ c.querySelectorAll('.obs-tab').forEach(tab => {
3033
+ tab.onclick = () => switchObsTab(tab.dataset.obstab);
3034
+ });
3035
+ switchObsTab(obsActiveTab);
2719
3036
  }
2720
3037
 
2721
3038
  function backToFeatureDetail() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.76",
3
+ "version": "0.3.78",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {