viberadar 0.3.215 → 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.
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +42 -12
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +251 -42
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -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-
|
|
1559
|
-
.load-
|
|
1560
|
-
|
|
1561
|
-
|
|
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 =
|
|
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');
|
|
@@ -7845,11 +7934,12 @@ function applyLoadImport(raw, sourceName) {
|
|
|
7845
7934
|
}
|
|
7846
7935
|
|
|
7847
7936
|
function loadImportConfigClick() {
|
|
7848
|
-
|
|
7849
|
-
|
|
7850
|
-
|
|
7851
|
-
|
|
7852
|
-
}
|
|
7937
|
+
openLoadImportConfigModal();
|
|
7938
|
+
}
|
|
7939
|
+
|
|
7940
|
+
function readLoadImportConfigText(text, sourceName) {
|
|
7941
|
+
const raw = JSON.parse(String(text || '{}'));
|
|
7942
|
+
applyLoadImport(raw, sourceName);
|
|
7853
7943
|
}
|
|
7854
7944
|
|
|
7855
7945
|
function loadImportConfigFile(input) {
|
|
@@ -7858,8 +7948,9 @@ function loadImportConfigFile(input) {
|
|
|
7858
7948
|
const reader = new FileReader();
|
|
7859
7949
|
reader.onload = () => {
|
|
7860
7950
|
try {
|
|
7861
|
-
|
|
7862
|
-
|
|
7951
|
+
readLoadImportConfigText(reader.result || '{}', file.name);
|
|
7952
|
+
const overlay = document.getElementById('loadImportConfigOverlay');
|
|
7953
|
+
if (overlay) overlay.remove();
|
|
7863
7954
|
} catch (e) {
|
|
7864
7955
|
alert('Конфиг не импортирован: ' + e.message);
|
|
7865
7956
|
}
|
|
@@ -7868,6 +7959,88 @@ function loadImportConfigFile(input) {
|
|
|
7868
7959
|
reader.readAsText(file);
|
|
7869
7960
|
}
|
|
7870
7961
|
|
|
7962
|
+
function openLoadImportConfigModal() {
|
|
7963
|
+
let overlay = document.getElementById('loadImportConfigOverlay');
|
|
7964
|
+
if (overlay) overlay.remove();
|
|
7965
|
+
overlay = document.createElement('div');
|
|
7966
|
+
overlay.id = 'loadImportConfigOverlay';
|
|
7967
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.62);display:flex;align-items:center;justify-content:center;z-index:9999;padding:18px';
|
|
7968
|
+
overlay.innerHTML = `
|
|
7969
|
+
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:8px;padding:18px;width:720px;max-width:96vw;max-height:92vh;overflow:auto">
|
|
7970
|
+
<div style="display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:10px">
|
|
7971
|
+
<div>
|
|
7972
|
+
<div style="font-size:15px;font-weight:600;color:var(--text)">Импорт k6 конфига</div>
|
|
7973
|
+
<div style="font-size:12px;color:var(--muted);margin-top:3px">Вставь JSON из буфера или выбери файл. После импорта поля можно править вручную.</div>
|
|
7974
|
+
</div>
|
|
7975
|
+
<button id="loadImportClose" class="load-btn" style="padding:4px 9px">Закрыть</button>
|
|
7976
|
+
</div>
|
|
7977
|
+
<textarea id="loadImportConfigText" spellcheck="false" placeholder='{"name":"auth-browse","baseUrl":"https://demo.unica.hopper-it.ru","vus":10,"duration":"2m","accounts":[{"email":"user@example.com","password":"pass"}],"script":"import http from ..."}' style="width:100%;min-height:260px;resize:vertical;background:var(--bg);border:1px solid var(--border);border-radius:6px;color:var(--text);font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px;line-height:1.45;padding:10px;outline:none"></textarea>
|
|
7978
|
+
<div id="loadImportStatus" style="min-height:18px;margin-top:8px;font-size:12px;color:var(--muted)">JSON пока не вставлен</div>
|
|
7979
|
+
<div style="display:flex;gap:8px;justify-content:space-between;align-items:center;margin-top:14px;flex-wrap:wrap">
|
|
7980
|
+
<div>
|
|
7981
|
+
<input id="loadConfigImportFile" type="file" accept=".json,application/json" style="display:none" onchange="loadImportConfigFile(this)" />
|
|
7982
|
+
<button class="load-btn" id="loadImportFileBtn">Выбрать JSON-файл</button>
|
|
7983
|
+
</div>
|
|
7984
|
+
<div style="display:flex;gap:8px">
|
|
7985
|
+
<button class="load-btn" id="loadImportCancel">Отмена</button>
|
|
7986
|
+
<button class="load-btn load-btn-run" id="loadImportApply" disabled>Импорт</button>
|
|
7987
|
+
</div>
|
|
7988
|
+
</div>
|
|
7989
|
+
</div>`;
|
|
7990
|
+
document.body.appendChild(overlay);
|
|
7991
|
+
|
|
7992
|
+
const textEl = overlay.querySelector('#loadImportConfigText');
|
|
7993
|
+
const statusEl = overlay.querySelector('#loadImportStatus');
|
|
7994
|
+
const applyBtn = overlay.querySelector('#loadImportApply');
|
|
7995
|
+
const close = () => overlay.remove();
|
|
7996
|
+
const validate = () => {
|
|
7997
|
+
const text = textEl.value.trim();
|
|
7998
|
+
if (!text) {
|
|
7999
|
+
statusEl.textContent = 'JSON пока не вставлен';
|
|
8000
|
+
statusEl.style.color = 'var(--muted)';
|
|
8001
|
+
applyBtn.disabled = true;
|
|
8002
|
+
return;
|
|
8003
|
+
}
|
|
8004
|
+
try {
|
|
8005
|
+
const raw = JSON.parse(text);
|
|
8006
|
+
const imported = normalizeLoadImport(raw, 'clipboard');
|
|
8007
|
+
const parts = [];
|
|
8008
|
+
if (imported.scriptName) parts.push(imported.scriptName);
|
|
8009
|
+
if (imported.baseUrl) parts.push(imported.baseUrl);
|
|
8010
|
+
if (imported.vus) parts.push(`${imported.vus} VUs`);
|
|
8011
|
+
if (imported.duration) parts.push(imported.duration);
|
|
8012
|
+
statusEl.textContent = `JSON валидный${parts.length ? ': ' + parts.join(' · ') : ''}`;
|
|
8013
|
+
statusEl.style.color = 'var(--green)';
|
|
8014
|
+
applyBtn.disabled = false;
|
|
8015
|
+
} catch (e) {
|
|
8016
|
+
statusEl.textContent = 'JSON невалидный: ' + e.message;
|
|
8017
|
+
statusEl.style.color = 'var(--red)';
|
|
8018
|
+
applyBtn.disabled = true;
|
|
8019
|
+
}
|
|
8020
|
+
};
|
|
8021
|
+
|
|
8022
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) close(); });
|
|
8023
|
+
overlay.querySelector('#loadImportClose').addEventListener('click', close);
|
|
8024
|
+
overlay.querySelector('#loadImportCancel').addEventListener('click', close);
|
|
8025
|
+
overlay.querySelector('#loadImportFileBtn').addEventListener('click', () => {
|
|
8026
|
+
const input = overlay.querySelector('#loadConfigImportFile');
|
|
8027
|
+
input.value = '';
|
|
8028
|
+
input.click();
|
|
8029
|
+
});
|
|
8030
|
+
textEl.addEventListener('input', validate);
|
|
8031
|
+
applyBtn.addEventListener('click', () => {
|
|
8032
|
+
try {
|
|
8033
|
+
readLoadImportConfigText(textEl.value, 'clipboard');
|
|
8034
|
+
close();
|
|
8035
|
+
} catch (e) {
|
|
8036
|
+
statusEl.textContent = 'Конфиг не импортирован: ' + e.message;
|
|
8037
|
+
statusEl.style.color = 'var(--red)';
|
|
8038
|
+
applyBtn.disabled = true;
|
|
8039
|
+
}
|
|
8040
|
+
});
|
|
8041
|
+
setTimeout(() => textEl.focus(), 0);
|
|
8042
|
+
}
|
|
8043
|
+
|
|
7871
8044
|
function renderLoad(c) {
|
|
7872
8045
|
const isRunning = loadState && loadState.status === 'running';
|
|
7873
8046
|
const isDone = loadState && (loadState.status === 'done' || loadState.status === 'stopped');
|
|
@@ -7928,9 +8101,8 @@ function renderLoad(c) {
|
|
|
7928
8101
|
<button class="load-btn load-btn-run" style="font-size:13px;padding:8px 20px" onclick="loadNewTest()">+ Новый тест</button>
|
|
7929
8102
|
</div>
|
|
7930
8103
|
</div>
|
|
7931
|
-
|
|
7932
|
-
|
|
7933
|
-
${loadSavedScripts.length === 0
|
|
8104
|
+
|
|
8105
|
+
${loadSavedScripts.length === 0
|
|
7934
8106
|
? `<div class="load-library-empty">
|
|
7935
8107
|
<div style="font-size:32px;margin-bottom:12px">📋</div>
|
|
7936
8108
|
<div style="font-size:14px;font-weight:500;margin-bottom:6px">Нет сохранённых тестов</div>
|
|
@@ -7972,15 +8144,17 @@ function renderLoad(c) {
|
|
|
7972
8144
|
loadState.status === 'stopped' ? '<span class="load-status-badge load-status-stopped">■ Остановлено</span>' :
|
|
7973
8145
|
'<span class="load-status-badge load-status-error">✗ Ошибка</span>';
|
|
7974
8146
|
|
|
7975
|
-
const summary = (loadState && loadState.summary) || {};
|
|
7976
|
-
const hasSummary = isDone && summary && Object.keys(summary).length > 0;
|
|
7977
|
-
|
|
7978
|
-
|
|
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">
|
|
7979
8154
|
|
|
7980
8155
|
<div class="load-editor-topbar">
|
|
7981
8156
|
${!isRunning ? `<button class="load-back-btn" onclick="loadView='library';renderContent()">← Все тесты</button>` : ''}
|
|
7982
8157
|
${!isRunning ? `<button class="load-btn" style="font-size:12px;padding:5px 10px" onclick="loadImportConfigClick()">Импорт конфига</button>` : ''}
|
|
7983
|
-
<input id="loadConfigImportFile" type="file" accept=".json,application/json" style="display:none" onchange="loadImportConfigFile(this)" />
|
|
7984
8158
|
<span style="font-size:12px;color:var(--muted);margin-left:4px">${statusBadge}</span>
|
|
7985
8159
|
</div>
|
|
7986
8160
|
|
|
@@ -8033,15 +8207,40 @@ function renderLoad(c) {
|
|
|
8033
8207
|
</div>
|
|
8034
8208
|
</div>
|
|
8035
8209
|
|
|
8036
|
-
${(isRunning || isDone || hasErr) ? `<div class="load-section">
|
|
8037
|
-
<div class="load-section-title">Живые метрики</div>
|
|
8038
|
-
<div class="load-
|
|
8039
|
-
<div class="load-
|
|
8040
|
-
|
|
8041
|
-
|
|
8042
|
-
|
|
8043
|
-
|
|
8044
|
-
|
|
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>` : ''}
|
|
8045
8244
|
|
|
8046
8245
|
${hasSummary ? `<div class="load-section">
|
|
8047
8246
|
<div class="load-section-title">Итоги</div>
|
|
@@ -8735,6 +8934,15 @@ function connectSSE() {
|
|
|
8735
8934
|
loadState = { runId, status: 'running', startTime: Date.now(), buckets: [], totalRequests: 0, totalErrors: 0, logs: [], script: loadScriptDraft || '', config, summary: null };
|
|
8736
8935
|
loadBuckets = [];
|
|
8737
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);
|
|
8738
8946
|
if (config) applyLoadConfigToFields(config);
|
|
8739
8947
|
if (contextMode === 'load') { renderSidebar(); renderContent(); }
|
|
8740
8948
|
});
|
|
@@ -8760,12 +8968,13 @@ function connectSSE() {
|
|
|
8760
8968
|
loadBuckets = buckets || [];
|
|
8761
8969
|
if (loadState) loadState.buckets = loadBuckets;
|
|
8762
8970
|
if (loadState) { loadState.totalRequests = total || 0; loadState.totalErrors = errors || 0; }
|
|
8763
|
-
if (contextMode === 'load') { drawLoadCharts(); renderSidebar(); }
|
|
8971
|
+
if (contextMode === 'load') { updateLoadLiveStats(); drawLoadCharts(); renderSidebar(); }
|
|
8764
8972
|
});
|
|
8765
8973
|
|
|
8766
8974
|
es.addEventListener('load-done', (e) => {
|
|
8767
8975
|
const { status, summary } = JSON.parse(e.data);
|
|
8768
8976
|
if (loadState) { loadState.status = status; loadState.summary = summary || null; loadState.endTime = Date.now(); }
|
|
8977
|
+
if (loadLiveUiTimer) { clearInterval(loadLiveUiTimer); loadLiveUiTimer = null; }
|
|
8769
8978
|
loadRefreshRuns();
|
|
8770
8979
|
if (contextMode === 'load') { renderSidebar(); renderContent(); }
|
|
8771
8980
|
});
|