viberadar 0.3.216 → 0.3.217

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.
@@ -1555,10 +1555,31 @@
1555
1555
  background: transparent; color: var(--muted); font-size: 11px; cursor: pointer;
1556
1556
  }
1557
1557
  .load-script-del-btn:hover { border-color: var(--red); color: var(--red); }
1558
- .load-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
1559
- .load-chart-box { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; }
1560
- .load-chart-label { font-size: 11px; color: var(--muted); margin-bottom: 6px; }
1561
- .load-chart-box canvas { display: block; width: 100%; height: 100px; }
1558
+ .load-live-shell { display: grid; grid-template-columns: minmax(180px, 240px) 1fr; gap: 14px; align-items: stretch; }
1559
+ .load-progress-card {
1560
+ background: linear-gradient(180deg, rgba(88,166,255,0.10), rgba(13,17,23,0));
1561
+ border: 1px solid rgba(88,166,255,0.28); border-radius: 8px; padding: 16px;
1562
+ display: flex; flex-direction: column; justify-content: space-between; min-height: 170px;
1563
+ }
1564
+ .load-progress-value { font-size: 52px; line-height: 1; font-weight: 800; color: var(--text); letter-spacing: 0; }
1565
+ .load-progress-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-top: 6px; }
1566
+ .load-progress-bar { height: 8px; background: var(--bg); border: 1px solid var(--border); border-radius: 999px; overflow: hidden; margin-top: 14px; }
1567
+ .load-progress-fill { height: 100%; background: var(--blue); border-radius: inherit; transition: width 0.25s ease; }
1568
+ .load-live-kpis { display: grid; grid-template-columns: repeat(4, minmax(100px, 1fr)); gap: 8px; margin-bottom: 10px; }
1569
+ .load-live-kpi { background: var(--bg); border: 1px solid var(--border); border-radius: 7px; padding: 10px 12px; min-width: 0; }
1570
+ .load-live-kpi-val { font-size: 20px; font-weight: 750; color: var(--text); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1571
+ .load-live-kpi-lbl { font-size: 10px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.4px; margin-top: 2px; }
1572
+ .load-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
1573
+ .load-chart-box { background: var(--bg); border: 1px solid var(--border); border-radius: 7px; padding: 10px; min-width: 0; }
1574
+ .load-chart-label { display:flex; align-items:center; justify-content:space-between; gap:8px; font-size: 11px; color: var(--muted); margin-bottom: 6px; }
1575
+ .load-chart-value { color: var(--text); font-weight: 700; }
1576
+ .load-chart-box canvas { display: block; width: 100%; height: 128px; }
1577
+ .load-chart-empty { height:128px; display:flex; align-items:center; justify-content:center; color:var(--dim); font-size:12px; border:1px dashed var(--border); border-radius:6px; }
1578
+ @media (max-width: 760px) {
1579
+ .load-live-shell { grid-template-columns: 1fr; }
1580
+ .load-live-kpis { grid-template-columns: 1fr 1fr; }
1581
+ .load-charts { grid-template-columns: 1fr; }
1582
+ }
1562
1583
  .load-summary-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(110px, 1fr)); gap: 8px; }
1563
1584
  .load-kpi { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; text-align: center; }
1564
1585
  .load-kpi-val { font-size: 20px; font-weight: 700; }
@@ -1794,6 +1815,7 @@ let loadDataDirDraft = '';
1794
1815
  let loadResultDirDraft = '';
1795
1816
  let loadAccountsJsonDraft = '';
1796
1817
  let loadImportedEnvVarsDraft = {};
1818
+ let loadLiveUiTimer = null;
1797
1819
  let loadView = 'library'; // 'library' | 'editor'
1798
1820
 
1799
1821
  function toggleObsHint(id) {
@@ -7660,20 +7682,87 @@ function generateScriptFromFeature(featureKey) {
7660
7682
  });
7661
7683
  }
7662
7684
 
7663
- function drawLoadCharts() {
7664
- if (!loadBuckets || loadBuckets.length === 0) return;
7665
- drawLoadChart('loadChartRps', loadBuckets, b => b.count / 2, 'var(--blue)', 'RPS');
7666
- drawLoadChart('loadChartLatency', loadBuckets, b => b.count > 0 ? b.durSum / b.count : 0, 'var(--green)', 'Latency avg (ms)');
7667
- drawLoadChart('loadChartErrors', loadBuckets, b => b.count > 0 ? (b.errors / b.count) * 100 : 0, 'var(--red)', 'Error %');
7668
- drawLoadChart('loadChartVus', loadBuckets, b => b.vus, 'var(--yellow)', 'VUs');
7669
- }
7670
-
7685
+ function drawLoadCharts() {
7686
+ if (!loadBuckets || loadBuckets.length === 0) return;
7687
+ drawLoadChart('loadChartRps', loadBuckets, b => b.count / 2, 'var(--blue)', 'RPS');
7688
+ drawLoadChart('loadChartLatency', loadBuckets, b => b.count > 0 ? b.durSum / b.count : 0, 'var(--green)', 'Latency avg (ms)');
7689
+ drawLoadChart('loadChartErrors', loadBuckets, b => b.count > 0 ? (b.errors / b.count) * 100 : 0, 'var(--red)', 'Error %');
7690
+ drawLoadChart('loadChartVus', loadBuckets, b => b.vus, 'var(--yellow)', 'VUs');
7691
+ }
7692
+
7693
+ function parseLoadDurationMs(value) {
7694
+ const text = String(value || '').trim();
7695
+ if (!text) return 0;
7696
+ let total = 0;
7697
+ const re = /(\d+(?:\.\d+)?)\s*(ms|s|m|h)/gi;
7698
+ let match;
7699
+ while ((match = re.exec(text))) {
7700
+ const n = Number(match[1]);
7701
+ const unit = match[2].toLowerCase();
7702
+ total += unit === 'h' ? n * 3600000 : unit === 'm' ? n * 60000 : unit === 's' ? n * 1000 : n;
7703
+ }
7704
+ if (total > 0) return total;
7705
+ const asNum = Number(text);
7706
+ return Number.isFinite(asNum) ? asNum * 1000 : 0;
7707
+ }
7708
+
7709
+ function estimateLoadDurationMs(state) {
7710
+ const cfg = state?.config || {};
7711
+ const base = parseLoadDurationMs(cfg.duration || loadDurationDraft);
7712
+ if (!base) return 0;
7713
+ return cfg.executionMode === 'script' ? base + 60000 : base;
7714
+ }
7715
+
7716
+ function formatLoadTime(ms) {
7717
+ if (!ms || ms < 0) return '0s';
7718
+ const sec = Math.floor(ms / 1000);
7719
+ const m = Math.floor(sec / 60);
7720
+ const s = sec % 60;
7721
+ return m > 0 ? `${m}m ${String(s).padStart(2, '0')}s` : `${s}s`;
7722
+ }
7723
+
7724
+ function getLoadLiveStats() {
7725
+ const state = loadState || {};
7726
+ const buckets = loadBuckets || [];
7727
+ const last = buckets[buckets.length - 1] || { count: 0, errors: 0, durSum: 0, vus: 0 };
7728
+ const total = state.totalRequests || buckets.reduce((sum, b) => sum + (b.count || 0), 0);
7729
+ const errors = state.totalErrors || buckets.reduce((sum, b) => sum + (b.errors || 0), 0);
7730
+ const recent = buckets.slice(-5);
7731
+ const recentReqs = recent.reduce((sum, b) => sum + (b.count || 0), 0);
7732
+ const recentErrors = recent.reduce((sum, b) => sum + (b.errors || 0), 0);
7733
+ const recentDur = recent.reduce((sum, b) => sum + (b.durSum || 0), 0);
7734
+ const currentRps = last.count ? last.count / 2 : 0;
7735
+ const avgMs = recentReqs > 0 ? recentDur / recentReqs : 0;
7736
+ const errorPct = recentReqs > 0 ? (recentErrors / recentReqs) * 100 : (total > 0 ? (errors / total) * 100 : 0);
7737
+ const start = state.startTime || Date.now();
7738
+ const end = state.endTime || Date.now();
7739
+ const elapsedMs = Math.max(0, end - start);
7740
+ const estimateMs = estimateLoadDurationMs(state);
7741
+ let progressPct = estimateMs > 0 ? Math.min(99, Math.round((elapsedMs / estimateMs) * 100)) : 0;
7742
+ if (state.status === 'done' || state.status === 'stopped' || state.status === 'error') progressPct = 100;
7743
+ return { total, errors, currentRps, avgMs, errorPct, vus: last.vus || 0, elapsedMs, estimateMs, progressPct, hasBuckets: buckets.length > 0 };
7744
+ }
7745
+
7746
+ function updateLoadLiveStats() {
7747
+ const stats = getLoadLiveStats();
7748
+ const setText = (id, text) => { const el = document.getElementById(id); if (el) el.textContent = text; };
7749
+ setText('loadProgressValue', `${stats.progressPct}%`);
7750
+ setText('loadElapsedValue', stats.estimateMs ? `${formatLoadTime(stats.elapsedMs)} / ${formatLoadTime(stats.estimateMs)}` : formatLoadTime(stats.elapsedMs));
7751
+ setText('loadTotalRequestsValue', String(Math.round(stats.total || 0)));
7752
+ setText('loadRpsValue', stats.currentRps.toFixed(1));
7753
+ setText('loadAvgValue', stats.avgMs ? `${Math.round(stats.avgMs)} ms` : '—');
7754
+ setText('loadErrorValue', `${stats.errorPct.toFixed(2)}%`);
7755
+ setText('loadVuValue', String(Math.round(stats.vus || 0)));
7756
+ const fill = document.getElementById('loadProgressFill');
7757
+ if (fill) fill.style.width = `${stats.progressPct}%`;
7758
+ }
7759
+
7671
7760
  function drawLoadChart(id, buckets, valFn, color, label) {
7672
- const canvas = document.getElementById(id);
7673
- if (!canvas) return;
7674
- const dpr = window.devicePixelRatio || 1;
7675
- const W = canvas.offsetWidth || 380;
7676
- const H = 100;
7761
+ const canvas = document.getElementById(id);
7762
+ if (!canvas) return;
7763
+ const dpr = window.devicePixelRatio || 1;
7764
+ const W = canvas.offsetWidth || 380;
7765
+ const H = 128;
7677
7766
  canvas.width = W * dpr;
7678
7767
  canvas.height = H * dpr;
7679
7768
  const ctx = canvas.getContext('2d');
@@ -8055,10 +8144,13 @@ function renderLoad(c) {
8055
8144
  loadState.status === 'stopped' ? '<span class="load-status-badge load-status-stopped">■ Остановлено</span>' :
8056
8145
  '<span class="load-status-badge load-status-error">✗ Ошибка</span>';
8057
8146
 
8058
- const summary = (loadState && loadState.summary) || {};
8059
- const hasSummary = isDone && summary && Object.keys(summary).length > 0;
8060
-
8061
- c.innerHTML = `<div class="load-screen">
8147
+ const summary = (loadState && loadState.summary) || {};
8148
+ const hasSummary = isDone && summary && Object.keys(summary).length > 0;
8149
+ const liveStats = getLoadLiveStats();
8150
+ const elapsedText = liveStats.estimateMs ? `${formatLoadTime(liveStats.elapsedMs)} / ${formatLoadTime(liveStats.estimateMs)}` : formatLoadTime(liveStats.elapsedMs);
8151
+ const chartEmpty = '<div class="load-chart-empty">Жду первые точки k6 metrics.ndjson</div>';
8152
+
8153
+ c.innerHTML = `<div class="load-screen">
8062
8154
 
8063
8155
  <div class="load-editor-topbar">
8064
8156
  ${!isRunning ? `<button class="load-back-btn" onclick="loadView='library';renderContent()">← Все тесты</button>` : ''}
@@ -8115,15 +8207,40 @@ function renderLoad(c) {
8115
8207
  </div>
8116
8208
  </div>
8117
8209
 
8118
- ${(isRunning || isDone || hasErr) ? `<div class="load-section">
8119
- <div class="load-section-title">Живые метрики</div>
8120
- <div class="load-charts">
8121
- <div class="load-chart-box"><div class="load-chart-label">Запросов/сек (RPS)</div><canvas id="loadChartRps"></canvas></div>
8122
- <div class="load-chart-box"><div class="load-chart-label">Среднее время ответа (мс)</div><canvas id="loadChartLatency"></canvas></div>
8123
- <div class="load-chart-box"><div class="load-chart-label">Процент ошибок (%)</div><canvas id="loadChartErrors"></canvas></div>
8124
- <div class="load-chart-box"><div class="load-chart-label">Активных VUs</div><canvas id="loadChartVus"></canvas></div>
8125
- </div>
8126
- </div>` : ''}
8210
+ ${(isRunning || isDone || hasErr) ? `<div class="load-section">
8211
+ <div class="load-section-title">Живые метрики</div>
8212
+ <div class="load-live-shell">
8213
+ <div class="load-progress-card">
8214
+ <div>
8215
+ <div class="load-progress-value" id="loadProgressValue">${liveStats.progressPct}%</div>
8216
+ <div class="load-progress-label">Выполнение прогона</div>
8217
+ <div class="load-progress-bar"><div class="load-progress-fill" id="loadProgressFill" style="width:${liveStats.progressPct}%"></div></div>
8218
+ </div>
8219
+ <div style="margin-top:14px">
8220
+ <div class="load-live-kpi-lbl">Время</div>
8221
+ <div class="load-live-kpi-val" id="loadElapsedValue" style="font-size:18px">${escapeHtml(elapsedText)}</div>
8222
+ </div>
8223
+ </div>
8224
+ <div>
8225
+ <div class="load-live-kpis">
8226
+ <div class="load-live-kpi"><div class="load-live-kpi-val" id="loadTotalRequestsValue">${Math.round(liveStats.total || 0)}</div><div class="load-live-kpi-lbl">Запросов</div></div>
8227
+ <div class="load-live-kpi"><div class="load-live-kpi-val" id="loadRpsValue">${liveStats.currentRps.toFixed(1)}</div><div class="load-live-kpi-lbl">RPS сейчас</div></div>
8228
+ <div class="load-live-kpi"><div class="load-live-kpi-val" id="loadAvgValue">${liveStats.avgMs ? `${Math.round(liveStats.avgMs)} ms` : '—'}</div><div class="load-live-kpi-lbl">Avg latency</div></div>
8229
+ <div class="load-live-kpi"><div class="load-live-kpi-val" id="loadErrorValue" style="color:${liveStats.errorPct > 2 ? 'var(--red)' : 'var(--text)'}">${liveStats.errorPct.toFixed(2)}%</div><div class="load-live-kpi-lbl">Ошибок</div></div>
8230
+ </div>
8231
+ <div class="load-live-kpi" style="margin-bottom:10px;display:flex;align-items:center;justify-content:space-between">
8232
+ <div><div class="load-live-kpi-lbl">Активные VUs</div><div class="load-live-kpi-val" id="loadVuValue">${Math.round(liveStats.vus || 0)}</div></div>
8233
+ <div style="font-size:12px;color:var(--muted);text-align:right">Данные обновляются из <code>metrics.ndjson</code></div>
8234
+ </div>
8235
+ </div>
8236
+ </div>
8237
+ <div class="load-charts" style="margin-top:12px">
8238
+ <div class="load-chart-box"><div class="load-chart-label"><span>Запросов/сек</span><span class="load-chart-value">${liveStats.currentRps.toFixed(1)}</span></div>${liveStats.hasBuckets ? '<canvas id="loadChartRps"></canvas>' : chartEmpty}</div>
8239
+ <div class="load-chart-box"><div class="load-chart-label"><span>Среднее время ответа</span><span class="load-chart-value">${liveStats.avgMs ? `${Math.round(liveStats.avgMs)} ms` : '—'}</span></div>${liveStats.hasBuckets ? '<canvas id="loadChartLatency"></canvas>' : chartEmpty}</div>
8240
+ <div class="load-chart-box"><div class="load-chart-label"><span>Ошибки</span><span class="load-chart-value">${liveStats.errorPct.toFixed(2)}%</span></div>${liveStats.hasBuckets ? '<canvas id="loadChartErrors"></canvas>' : chartEmpty}</div>
8241
+ <div class="load-chart-box"><div class="load-chart-label"><span>Активные VUs</span><span class="load-chart-value">${Math.round(liveStats.vus || 0)}</span></div>${liveStats.hasBuckets ? '<canvas id="loadChartVus"></canvas>' : chartEmpty}</div>
8242
+ </div>
8243
+ </div>` : ''}
8127
8244
 
8128
8245
  ${hasSummary ? `<div class="load-section">
8129
8246
  <div class="load-section-title">Итоги</div>
@@ -8817,6 +8934,15 @@ function connectSSE() {
8817
8934
  loadState = { runId, status: 'running', startTime: Date.now(), buckets: [], totalRequests: 0, totalErrors: 0, logs: [], script: loadScriptDraft || '', config, summary: null };
8818
8935
  loadBuckets = [];
8819
8936
  loadLogLines = [];
8937
+ if (loadLiveUiTimer) clearInterval(loadLiveUiTimer);
8938
+ loadLiveUiTimer = setInterval(() => {
8939
+ if (!loadState || loadState.status !== 'running') {
8940
+ clearInterval(loadLiveUiTimer);
8941
+ loadLiveUiTimer = null;
8942
+ return;
8943
+ }
8944
+ if (contextMode === 'load') updateLoadLiveStats();
8945
+ }, 1000);
8820
8946
  if (config) applyLoadConfigToFields(config);
8821
8947
  if (contextMode === 'load') { renderSidebar(); renderContent(); }
8822
8948
  });
@@ -8842,12 +8968,13 @@ function connectSSE() {
8842
8968
  loadBuckets = buckets || [];
8843
8969
  if (loadState) loadState.buckets = loadBuckets;
8844
8970
  if (loadState) { loadState.totalRequests = total || 0; loadState.totalErrors = errors || 0; }
8845
- if (contextMode === 'load') { drawLoadCharts(); renderSidebar(); }
8971
+ if (contextMode === 'load') { updateLoadLiveStats(); drawLoadCharts(); renderSidebar(); }
8846
8972
  });
8847
8973
 
8848
8974
  es.addEventListener('load-done', (e) => {
8849
8975
  const { status, summary } = JSON.parse(e.data);
8850
8976
  if (loadState) { loadState.status = status; loadState.summary = summary || null; loadState.endTime = Date.now(); }
8977
+ if (loadLiveUiTimer) { clearInterval(loadLiveUiTimer); loadLiveUiTimer = null; }
8851
8978
  loadRefreshRuns();
8852
8979
  if (contextMode === 'load') { renderSidebar(); renderContent(); }
8853
8980
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.216",
3
+ "version": "0.3.217",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {