viberadar 0.3.78 → 0.3.80

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.
@@ -787,6 +787,78 @@
787
787
  .obs-hint-label.before { color: var(--red); }
788
788
  .obs-hint-label.after { color: var(--green); }
789
789
 
790
+ /* ── Progress & baseline delta badges ── */
791
+ .obs-kpi-delta {
792
+ display: block; font-size: 11px; font-weight: 700; margin-top: 2px; letter-spacing: 0.01em;
793
+ }
794
+ .obs-baseline-strip {
795
+ display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
796
+ padding: 4px 12px; background: rgba(255,255,255,0.02);
797
+ border: 1px solid rgba(255,255,255,0.07); border-radius: 6px;
798
+ font-size: 11px; color: var(--dim); margin-bottom: 6px;
799
+ }
800
+ .obs-baseline-strip button {
801
+ background: none; border: none; color: var(--accent); cursor: pointer;
802
+ font-size: 11px; padding: 0 2px; text-decoration: underline; text-underline-offset: 2px;
803
+ }
804
+ .obs-progress-banner {
805
+ display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
806
+ padding: 8px 14px;
807
+ background: rgba(63,185,80,0.07); border: 1px solid rgba(63,185,80,0.2);
808
+ border-radius: 8px; margin-bottom: 8px; font-size: 12px;
809
+ }
810
+ .obs-progress-banner.has-regression {
811
+ background: rgba(248,81,73,0.07); border-color: rgba(248,81,73,0.2);
812
+ }
813
+ .obs-pb-title { color: #3fb950; font-weight: 700; font-size: 13px; flex-shrink: 0; }
814
+ .obs-pb-title.mixed { color: var(--yellow); }
815
+ .obs-pb-item { color: #3fb950; white-space: nowrap; }
816
+ .obs-pb-worse { color: #f85149; white-space: nowrap; }
817
+
818
+ /* ── Floating selection action bar ── */
819
+ .obs-floating-bar {
820
+ position: fixed;
821
+ bottom: -80px;
822
+ left: 50%;
823
+ transform: translateX(-50%);
824
+ z-index: 300;
825
+ display: flex; align-items: center; gap: 12px;
826
+ background: #1c2128;
827
+ border: 1px solid var(--accent);
828
+ border-radius: 10px;
829
+ padding: 10px 10px 10px 16px;
830
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5), 0 0 0 1px rgba(88,166,255,0.15);
831
+ min-width: 340px;
832
+ transition: bottom 0.25s cubic-bezier(0.34,1.56,0.64,1);
833
+ pointer-events: none;
834
+ opacity: 0;
835
+ }
836
+ .obs-floating-bar.visible {
837
+ bottom: 28px;
838
+ pointer-events: auto;
839
+ opacity: 1;
840
+ }
841
+ .obs-floating-count {
842
+ color: var(--text); font-size: 13px; font-weight: 600; white-space: nowrap;
843
+ }
844
+ .obs-floating-label {
845
+ color: var(--muted); font-size: 12px; flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
846
+ }
847
+ .obs-floating-btn {
848
+ padding: 8px 18px; font-size: 13px; font-weight: 600;
849
+ background: var(--accent); color: var(--bg);
850
+ border: none; border-radius: 6px; cursor: pointer; min-height: 36px;
851
+ white-space: nowrap; flex-shrink: 0;
852
+ transition: opacity 0.1s;
853
+ }
854
+ .obs-floating-btn:hover { opacity: 0.85; }
855
+ .obs-floating-dismiss {
856
+ background: none; border: none; color: var(--dim); cursor: pointer;
857
+ font-size: 16px; padding: 4px 6px; line-height: 1; flex-shrink: 0;
858
+ border-radius: 4px; transition: color 0.1s;
859
+ }
860
+ .obs-floating-dismiss:hover { color: var(--text); }
861
+
790
862
  .file-row-err-badge {
791
863
  display: inline-flex; align-items: center;
792
864
  font-size: 11px; padding: 1px 6px; border-radius: 10px;
@@ -1219,6 +1291,13 @@
1219
1291
  <div id="panelContent"></div>
1220
1292
  </div>
1221
1293
 
1294
+ <div class="obs-floating-bar" id="obsFloatingBar">
1295
+ <span class="obs-floating-count" id="obsFloatingCount">0 выбрано</span>
1296
+ <span class="obs-floating-label" id="obsFloatingLabel"></span>
1297
+ <button class="obs-floating-btn" id="obsFloatingBtn" onclick="obsDispatchFloating()">запустить</button>
1298
+ <button class="obs-floating-dismiss" title="Снять выделение" onclick="obsFloatingDismiss()">✕</button>
1299
+ </div>
1300
+
1222
1301
  <div class="agent-panel" id="agentPanel">
1223
1302
  <div class="agent-panel-header">
1224
1303
  <span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
@@ -2237,9 +2316,76 @@ function obsUpdateSelectedCount(groupId) {
2237
2316
  const checked = container.querySelectorAll('input[type="checkbox"]:checked').length;
2238
2317
  const btn = container.querySelector('.obs-run-selected');
2239
2318
  if (btn) {
2240
- btn.textContent = checked > 0 ? `исправить выбранные (${checked})` : 'исправить выбранные';
2319
+ const baseLabel = btn.dataset.baseLabel || btn.textContent.replace(/\s*\(\d+\)$/, '');
2320
+ btn.dataset.baseLabel = baseLabel;
2321
+ btn.textContent = checked > 0 ? `${baseLabel} (${checked})` : baseLabel;
2241
2322
  btn.disabled = checked === 0;
2242
2323
  }
2324
+ // Update floating action bar
2325
+ const bar = document.getElementById('obsFloatingBar');
2326
+ if (!bar) return;
2327
+ if (checked > 0) {
2328
+ const action = container.dataset.obsAction || '';
2329
+ const actionLabels = { missing: 'добавить логи', field: 'обогатить поля', rec: 'исправить' };
2330
+ const groupLabel = container.dataset.obsLabel || '';
2331
+ bar.dataset.groupId = groupId;
2332
+ document.getElementById('obsFloatingCount').textContent = `${checked} выбрано`;
2333
+ document.getElementById('obsFloatingLabel').textContent = groupLabel;
2334
+ document.getElementById('obsFloatingBtn').textContent = actionLabels[action] || 'исправить';
2335
+ bar.classList.add('visible');
2336
+ } else {
2337
+ if (bar.dataset.groupId === groupId) {
2338
+ bar.classList.remove('visible');
2339
+ delete bar.dataset.groupId;
2340
+ }
2341
+ }
2342
+ }
2343
+
2344
+ function obsDispatchFloating() {
2345
+ const bar = document.getElementById('obsFloatingBar');
2346
+ if (!bar) return;
2347
+ const groupId = bar.dataset.groupId;
2348
+ if (!groupId) return;
2349
+ const container = document.getElementById('obs-detail-' + groupId);
2350
+ if (!container) return;
2351
+ const action = container.dataset.obsAction || '';
2352
+ if (action === 'missing') {
2353
+ obsMissingRunSelected(groupId);
2354
+ } else {
2355
+ const meta = container.dataset.obsMeta ? JSON.parse(container.dataset.obsMeta) : {};
2356
+ obsRunSelected(groupId, 'obs-fix-selected', meta);
2357
+ }
2358
+ bar.classList.remove('visible');
2359
+ }
2360
+
2361
+ function obsTimeAgo(ts) {
2362
+ const s = Math.floor((Date.now() - ts) / 1000);
2363
+ if (s < 60) return `${s}с назад`;
2364
+ if (s < 3600) return `${Math.floor(s / 60)} мин назад`;
2365
+ if (s < 86400) return `${Math.floor(s / 3600)}ч назад`;
2366
+ return `${Math.floor(s / 86400)}д назад`;
2367
+ }
2368
+
2369
+ function obsResetBaseline(key) {
2370
+ try { localStorage.removeItem('vr_obs_bl_' + key); } catch(e) {}
2371
+ const c = document.getElementById('content');
2372
+ if (c) renderObservability(c);
2373
+ }
2374
+
2375
+ function obsFloatingDismiss() {
2376
+ const bar = document.getElementById('obsFloatingBar');
2377
+ if (!bar) return;
2378
+ const groupId = bar.dataset.groupId;
2379
+ if (groupId) {
2380
+ const container = document.getElementById('obs-detail-' + groupId);
2381
+ if (container) {
2382
+ container.querySelectorAll('input[type="checkbox"]').forEach(cb => cb.checked = false);
2383
+ const btn = container.querySelector('.obs-run-selected');
2384
+ if (btn) { btn.textContent = btn.dataset.baseLabel || btn.textContent.replace(/\s*\(\d+\)$/, ''); btn.disabled = true; }
2385
+ }
2386
+ delete bar.dataset.groupId;
2387
+ }
2388
+ bar.classList.remove('visible');
2243
2389
  }
2244
2390
 
2245
2391
  function obsToggleAll(groupId) {
@@ -2576,6 +2722,47 @@ function renderObservability(c) {
2576
2722
  return val < warnThreshold ? 'var(--red)' : val < goodThreshold ? 'var(--yellow)' : 'var(--green)';
2577
2723
  }
2578
2724
 
2725
+ // ── Baseline & delta tracking ──
2726
+ const _bsKey = String(D.currentFeatureKey || 'global').replace(/[^a-z0-9_-]/gi, '_');
2727
+ let _bl = null;
2728
+ try { _bl = JSON.parse(localStorage.getItem('vr_obs_bl_' + _bsKey) || 'null'); } catch(e) {}
2729
+
2730
+ // Pre-compute current snapshot values (before tierSections so _d is in scope)
2731
+ const _blCurrMissing = (o.missingCriticalLogsV2 || []).length;
2732
+ const _blCurrFPs = (o.missingCriticalLogsV2 || []).reduce((s, m) => s + (m.failurePoints || []).length, 0);
2733
+ const _blCurrNoise = (o.topNoisyPatterns || []).length;
2734
+ const _blCurrEnrich = Object.entries(o.fieldGaps || {}).filter(([, v]) => v > 0).length;
2735
+ const _blCurrTiers = {};
2736
+ ['critical', 'important', 'normal'].forEach(tier => {
2737
+ const items = (o.missingCriticalLogsV2 || []).filter(m => m.riskTier === tier);
2738
+ _blCurrTiers[tier] = { modules: items.length, fps: items.reduce((s, m) => s + (m.failurePoints || []).length, 0) };
2739
+ });
2740
+
2741
+ if (!_bl) {
2742
+ _bl = { ts: Date.now(), noiseRatio, structPct, actionPct, coveragePct,
2743
+ missing: _blCurrMissing, fps: _blCurrFPs, noise: _blCurrNoise, enrich: _blCurrEnrich,
2744
+ tiers: _blCurrTiers };
2745
+ try { localStorage.setItem('vr_obs_bl_' + _bsKey, JSON.stringify(_bl)); } catch(e) {}
2746
+ }
2747
+
2748
+ // Delta badge helpers
2749
+ const _d = (curr, base, invert = false) => {
2750
+ if (base == null || curr === base) return '';
2751
+ const diff = curr - base;
2752
+ const good = invert ? diff > 0 : diff < 0;
2753
+ const color = good ? '#3fb950' : '#f85149';
2754
+ const sign = diff > 0 ? '+' : '';
2755
+ return `<span style="color:${color};font-size:11px;font-weight:700;margin-left:5px">(${sign}${diff})</span>`;
2756
+ };
2757
+ const _dp = (curr, base, invert = false) => {
2758
+ if (base == null || curr === base) return '';
2759
+ const diff = curr - base;
2760
+ const good = invert ? diff > 0 : diff < 0;
2761
+ const color = good ? '#3fb950' : '#f85149';
2762
+ const arrow = good ? '↓' : '↑';
2763
+ return `<span class="obs-kpi-delta" style="color:${color}">${arrow}${Math.abs(diff)}%</span>`;
2764
+ };
2765
+
2579
2766
  const formatLabels = { structured: 'structured', mixed: 'mixed', unstructured: 'unstructured' };
2580
2767
  const sourceByFormat = [
2581
2768
  { label: 'Structured', count: o.catalog.filter(c => c.format === 'structured').length, color: 'var(--green)' },
@@ -2645,17 +2832,18 @@ function renderObservability(c) {
2645
2832
  }).join('');
2646
2833
  const addBtn = hasAgent ? `<button class="obs-run-selected" disabled onclick="obsMissingRunSelected('${groupId}')">добавить логи выбранным</button>` : '';
2647
2834
  const detail = hasAgent ? `
2648
- <div id="obs-detail-${groupId}" class="obs-detail">
2835
+ <div id="obs-detail-${groupId}" class="obs-detail" data-obs-action="missing" data-obs-label="${tierLabels[tier]}">
2649
2836
  <div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
2650
2837
  <button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
2651
2838
  </div>
2652
2839
  <div class="obs-detail-list">${detailItems}</div>
2653
2840
  <div class="obs-detail-bar">${addBtn}</div>
2654
2841
  </div>` : '';
2842
+ const _blTier = _bl?.tiers?.[tier];
2655
2843
  return `<div class="obs-tier-group">
2656
2844
  <div class="obs-tier-group-header">
2657
2845
  <span class="obs-tier-badge obs-tier-${tier}">${tierLabels[tier]}</span>
2658
- <span>${items.length} модулей, ${totalFPs} точек отказа</span>
2846
+ <span>${items.length}${_d(items.length, _blTier?.modules)} модулей, ${totalFPs}${_d(totalFPs, _blTier?.fps)} точек отказа</span>
2659
2847
  ${expandBtn}
2660
2848
  </div>
2661
2849
  ${detail}
@@ -2677,7 +2865,7 @@ function renderObservability(c) {
2677
2865
  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>`;
2678
2866
  }).join('');
2679
2867
  const detail = hasAgent ? `
2680
- <div id="obs-detail-${groupId}" class="obs-detail">
2868
+ <div id="obs-detail-${groupId}" class="obs-detail" data-obs-action="field" data-obs-meta='{"fieldName":"${escapeHtml(name)}"}' data-obs-label="поле ${escapeHtml(name)}">
2681
2869
  <div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
2682
2870
  <button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
2683
2871
  </div>
@@ -2718,27 +2906,59 @@ function renderObservability(c) {
2718
2906
  const enrichCount = fieldGapEntries.length;
2719
2907
  const catalogCount = o.catalog.length;
2720
2908
 
2909
+ // ── Build progress banner comparing to baseline ──
2910
+ const _blItems = [];
2911
+ if (_bl.missing != null && missingCount !== _bl.missing)
2912
+ _blItems.push({ label: `модулей без логов: ${_bl.missing} → ${missingCount}`, good: missingCount < _bl.missing });
2913
+ if (_bl.fps != null && _blCurrFPs !== _bl.fps)
2914
+ _blItems.push({ label: `точек отказа: ${_bl.fps} → ${_blCurrFPs}`, good: _blCurrFPs < _bl.fps });
2915
+ if (_bl.noise != null && noisyCount !== _bl.noise)
2916
+ _blItems.push({ label: `шумных паттернов: ${_bl.noise} → ${noisyCount}`, good: noisyCount < _bl.noise });
2917
+ if (_bl.enrich != null && enrichCount !== _bl.enrich)
2918
+ _blItems.push({ label: `нужно обогатить: ${_bl.enrich} → ${enrichCount}`, good: enrichCount < _bl.enrich });
2919
+
2920
+ const _hasReg = _blItems.some(x => !x.good);
2921
+ const _hasImp = _blItems.some(x => x.good);
2922
+ const _progressBanner = _blItems.length ? `
2923
+ <div class="obs-progress-banner${_hasReg ? ' has-regression' : ''}">
2924
+ <span class="obs-pb-title${_hasReg && _hasImp ? ' mixed' : _hasReg ? ' obs-pb-worse' : ''}">📊 прогресс</span>
2925
+ ${_blItems.map(it => `<span class="${it.good ? 'obs-pb-item' : 'obs-pb-worse'}">${it.good ? '✅' : '⚠️'} ${it.label}</span>`).join('')}
2926
+ </div>` : '';
2927
+
2928
+ const _baselineStrip = `
2929
+ <div class="obs-baseline-strip">
2930
+ <span>📍 baseline: ${obsTimeAgo(_bl.ts)}</span>
2931
+ <button onclick="obsResetBaseline('${_bsKey}')">обновить</button>
2932
+ </div>`;
2933
+
2721
2934
  c.innerHTML = `
2722
2935
  <div class="onboarding-block">
2723
2936
  <h3>Наблюдаемость: что это?</h3>
2724
2937
  <p>Аудит покрытия логами: что добавить, что убрать, что обогатить — на основе статического анализа лог-вызовов.</p>
2725
2938
  </div>
2726
2939
 
2940
+ ${_progressBanner}
2941
+ ${_baselineStrip}
2942
+
2727
2943
  <div class="obs-kpi-strip">
2728
2944
  <div class="obs-kpi">
2729
2945
  <span class="obs-kpi-value" style="color:${metricColor(noiseRatio,10,30,true)}">${noiseRatio}%</span>
2946
+ ${_dp(noiseRatio, _bl?.noiseRatio, true)}
2730
2947
  <span class="obs-kpi-label">Шум</span>
2731
2948
  </div>
2732
2949
  <div class="obs-kpi">
2733
2950
  <span class="obs-kpi-value" style="color:${metricColor(structPct,80,50,false)}">${structPct}%</span>
2951
+ ${_dp(structPct, _bl?.structPct)}
2734
2952
  <span class="obs-kpi-label">Структурированность</span>
2735
2953
  </div>
2736
2954
  <div class="obs-kpi">
2737
2955
  <span class="obs-kpi-value" style="color:${metricColor(actionPct,80,50,false)}">${actionPct}%</span>
2956
+ ${_dp(actionPct, _bl?.actionPct)}
2738
2957
  <span class="obs-kpi-label">Actionable ошибки</span>
2739
2958
  </div>
2740
2959
  <div class="obs-kpi">
2741
2960
  <span class="obs-kpi-value" style="color:${metricColor(coveragePct,80,50,false)}">${coveragePct}%</span>
2961
+ ${_dp(coveragePct, _bl?.coveragePct)}
2742
2962
  <span class="obs-kpi-label">Покрытие сценариев</span>
2743
2963
  </div>
2744
2964
  </div>
@@ -2814,7 +3034,7 @@ function renderObservability(c) {
2814
3034
  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
3035
  }).join('');
2816
3036
  const detail = hasAgent ? `
2817
- <div id="obs-detail-${groupId}" class="obs-detail">
3037
+ <div id="obs-detail-${groupId}" class="obs-detail" data-obs-action="rec" data-obs-meta='{"recommendationType":"${rec}"}' data-obs-label="${recLabels[rec] || rec}">
2818
3038
  <div class="obs-detail-bar" style="border-top:none;padding-top:0;margin-bottom:4px">
2819
3039
  <button class="obs-select-all" onclick="obsToggleAll('${groupId}')">выбрать все / снять</button>
2820
3040
  </div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.78",
3
+ "version": "0.3.80",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {