viberadar 0.3.213 → 0.3.215

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.
@@ -1513,6 +1513,12 @@
1513
1513
  .load-run-meta { color: var(--muted); font-size: 11px; margin-top: 2px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
1514
1514
  .load-run-kpi { color: var(--muted); font-size: 11px; white-space: nowrap; text-align: right; }
1515
1515
  .load-config-hint { font-size: 11px; color: var(--muted); margin-top: 8px; line-height: 1.45; }
1516
+ .load-accounts-json {
1517
+ width: 100%; min-height: 74px; background: var(--bg); border: 1px solid var(--border);
1518
+ border-radius: 5px; color: var(--text); font-family: 'Cascadia Code', Consolas, monospace;
1519
+ font-size: 12px; padding: 8px 10px; resize: vertical; outline: none; line-height: 1.45;
1520
+ }
1521
+ .load-accounts-json:focus { border-color: var(--blue); }
1516
1522
  .load-library-empty {
1517
1523
  display: flex; flex-direction: column; align-items: center; justify-content: center;
1518
1524
  padding: 60px 20px; text-align: center; color: var(--dim);
@@ -1786,6 +1792,8 @@ let loadVusDraft = 10;
1786
1792
  let loadDurationDraft = '30s';
1787
1793
  let loadDataDirDraft = '';
1788
1794
  let loadResultDirDraft = '';
1795
+ let loadAccountsJsonDraft = '';
1796
+ let loadImportedEnvVarsDraft = {};
1789
1797
  let loadView = 'library'; // 'library' | 'editor'
1790
1798
 
1791
1799
  function toggleObsHint(id) {
@@ -7605,13 +7613,10 @@ async function checkK6() {
7605
7613
  loadK6Available = d.available;
7606
7614
  loadK6Version = d.version || '';
7607
7615
  } catch { loadK6Available = false; }
7608
- if (contextMode === 'load') {
7609
- renderSidebar(); renderContent();
7610
- // restore saved token from localStorage
7611
- const saved = localStorage.getItem('vr_load_token');
7612
- if (saved) { const el = document.getElementById('loadToken'); if (el) el.value = saved; }
7613
- }
7614
- }
7616
+ if (contextMode === 'load') {
7617
+ renderSidebar(); renderContent();
7618
+ }
7619
+ }
7615
7620
 
7616
7621
  function buildDefaultScript(cfg) {
7617
7622
  const baseUrl = cfg.baseUrl || 'http://localhost:5000';
@@ -7763,16 +7768,104 @@ function applyLoadConfigToFields(cfg) {
7763
7768
  loadDurationDraft = cfg.duration || loadDurationDraft;
7764
7769
  loadDataDirDraft = cfg.dataDir || loadDataDirDraft;
7765
7770
  loadResultDirDraft = cfg.resultDir || loadResultDirDraft;
7771
+ if (cfg.accountsJson) loadAccountsJsonDraft = cfg.accountsJson;
7772
+ if (cfg.accounts) loadAccountsJsonDraft = typeof cfg.accounts === 'string' ? cfg.accounts : JSON.stringify(cfg.accounts, null, 2);
7766
7773
  const baseEl = document.getElementById('loadBaseUrl');
7767
7774
  const vusEl = document.getElementById('loadVus');
7768
7775
  const durEl = document.getElementById('loadDuration');
7769
7776
  const dataEl = document.getElementById('loadDataDir');
7770
7777
  const resultEl = document.getElementById('loadResultDir');
7778
+ const accountsEl = document.getElementById('loadAccountsJson');
7771
7779
  if (baseEl) baseEl.value = loadBaseUrlDraft;
7772
7780
  if (vusEl) vusEl.value = loadVusDraft;
7773
7781
  if (durEl) durEl.value = loadDurationDraft;
7774
7782
  if (dataEl) dataEl.value = loadDataDirDraft;
7775
7783
  if (resultEl) resultEl.value = loadResultDirDraft;
7784
+ if (accountsEl) accountsEl.value = loadAccountsJsonDraft;
7785
+ }
7786
+
7787
+ function normalizeAccountsJson(raw) {
7788
+ const text = (raw || '').trim();
7789
+ if (!text) return { text: '', accounts: [], error: null };
7790
+ try {
7791
+ const parsed = JSON.parse(text);
7792
+ const accounts = Array.isArray(parsed) ? parsed : [parsed];
7793
+ const normalized = accounts
7794
+ .map(a => ({
7795
+ email: String(a.email || a.login || a.username || '').trim(),
7796
+ password: String(a.password || '').trim(),
7797
+ }))
7798
+ .filter(a => a.email && a.password);
7799
+ if (!normalized.length) return { text, accounts: [], error: 'Accounts JSON должен содержать email/login и password' };
7800
+ return { text: JSON.stringify(normalized), accounts: normalized, error: null };
7801
+ } catch (e) {
7802
+ return { text, accounts: [], error: 'Accounts JSON невалидный: ' + e.message };
7803
+ }
7804
+ }
7805
+
7806
+ function normalizeLoadImport(raw, sourceName) {
7807
+ const root = raw && typeof raw === 'object' ? raw : {};
7808
+ const cfg = root.config && typeof root.config === 'object' ? { ...root.config, ...root } : root;
7809
+ const envVars = cfg.envVars && typeof cfg.envVars === 'object' && !Array.isArray(cfg.envVars) ? { ...cfg.envVars } : {};
7810
+ const imported = {
7811
+ scriptName: cfg.scriptName || cfg.name || (sourceName || '').replace(/\.json$/i, '') || '',
7812
+ script: cfg.script || cfg.k6Script || cfg.code || '',
7813
+ baseUrl: cfg.baseUrl || envVars.BASE_URL || '',
7814
+ vus: cfg.vus || cfg.VUs || cfg.users || '',
7815
+ duration: cfg.duration || cfg.testDuration || '',
7816
+ dataDir: cfg.dataDir || cfg.dataPath || '',
7817
+ resultDir: cfg.resultDir || cfg.resultsDir || '',
7818
+ accounts: cfg.accounts || cfg.usersJson || cfg.accountsJson || envVars.TEST_ACCOUNTS_JSON || '',
7819
+ envVars,
7820
+ };
7821
+ delete imported.envVars.BASE_URL;
7822
+ delete imported.envVars.TEST_ACCOUNTS_JSON;
7823
+ delete imported.envVars.TEST_EMAIL;
7824
+ delete imported.envVars.TEST_PASSWORD;
7825
+ return imported;
7826
+ }
7827
+
7828
+ function applyLoadImport(raw, sourceName) {
7829
+ const imported = normalizeLoadImport(raw, sourceName);
7830
+ if (imported.script) loadScriptDraft = String(imported.script);
7831
+ if (imported.scriptName) loadScriptNameDraft = String(imported.scriptName);
7832
+ if (imported.baseUrl) loadBaseUrlDraft = String(imported.baseUrl);
7833
+ if (imported.vus) loadVusDraft = parseInt(String(imported.vus), 10) || loadVusDraft;
7834
+ if (imported.duration) loadDurationDraft = String(imported.duration);
7835
+ if (imported.dataDir) loadDataDirDraft = String(imported.dataDir);
7836
+ if (imported.resultDir) loadResultDirDraft = String(imported.resultDir);
7837
+ if (imported.accounts) {
7838
+ loadAccountsJsonDraft = typeof imported.accounts === 'string'
7839
+ ? imported.accounts
7840
+ : JSON.stringify(imported.accounts, null, 2);
7841
+ }
7842
+ loadImportedEnvVarsDraft = imported.envVars || {};
7843
+ loadView = 'editor';
7844
+ renderContent();
7845
+ }
7846
+
7847
+ function loadImportConfigClick() {
7848
+ const input = document.getElementById('loadConfigImportFile');
7849
+ if (input) {
7850
+ input.value = '';
7851
+ input.click();
7852
+ }
7853
+ }
7854
+
7855
+ function loadImportConfigFile(input) {
7856
+ const file = input && input.files && input.files[0];
7857
+ if (!file) return;
7858
+ const reader = new FileReader();
7859
+ reader.onload = () => {
7860
+ try {
7861
+ const raw = JSON.parse(String(reader.result || '{}'));
7862
+ applyLoadImport(raw, file.name);
7863
+ } catch (e) {
7864
+ alert('Конфиг не импортирован: ' + e.message);
7865
+ }
7866
+ };
7867
+ reader.onerror = () => alert('Не удалось прочитать файл конфига');
7868
+ reader.readAsText(file);
7776
7869
  }
7777
7870
 
7778
7871
  function renderLoad(c) {
@@ -7828,10 +7921,14 @@ function renderLoad(c) {
7828
7921
  <div class="load-library-header">
7829
7922
  <div>
7830
7923
  <div style="font-size:16px;font-weight:600;color:var(--fg)">Нагрузочные тесты</div>
7831
- <div style="font-size:12px;color:var(--muted);margin-top:2px">${loadK6Version ? escapeHtml(loadK6Version) : ''} · ${loadSavedScripts.length} сохранённых</div>
7832
- </div>
7833
- <button class="load-btn load-btn-run" style="font-size:13px;padding:8px 20px" onclick="loadNewTest()">+ Новый тест</button>
7834
- </div>
7924
+ <div style="font-size:12px;color:var(--muted);margin-top:2px">${loadK6Version ? escapeHtml(loadK6Version) : ''} · ${loadSavedScripts.length} сохранённых</div>
7925
+ </div>
7926
+ <div class="load-btns" style="margin:0">
7927
+ <button class="load-btn" style="font-size:13px;padding:8px 14px" onclick="loadImportConfigClick()">Импорт конфига</button>
7928
+ <button class="load-btn load-btn-run" style="font-size:13px;padding:8px 20px" onclick="loadNewTest()">+ Новый тест</button>
7929
+ </div>
7930
+ </div>
7931
+ <input id="loadConfigImportFile" type="file" accept=".json,application/json" style="display:none" onchange="loadImportConfigFile(this)" />
7835
7932
 
7836
7933
  ${loadSavedScripts.length === 0
7837
7934
  ? `<div class="load-library-empty">
@@ -7880,10 +7977,12 @@ function renderLoad(c) {
7880
7977
 
7881
7978
  c.innerHTML = `<div class="load-screen">
7882
7979
 
7883
- <div class="load-editor-topbar">
7884
- ${!isRunning ? `<button class="load-back-btn" onclick="loadView='library';renderContent()">← Все тесты</button>` : ''}
7885
- <span style="font-size:12px;color:var(--muted);margin-left:4px">${statusBadge}</span>
7886
- </div>
7980
+ <div class="load-editor-topbar">
7981
+ ${!isRunning ? `<button class="load-back-btn" onclick="loadView='library';renderContent()">← Все тесты</button>` : ''}
7982
+ ${!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
+ <span style="font-size:12px;color:var(--muted);margin-left:4px">${statusBadge}</span>
7985
+ </div>
7887
7986
 
7888
7987
  <div class="load-section">
7889
7988
  <div class="load-section-title">Конфигурация ${loadK6Version ? `<span style="color:var(--dim);font-weight:400">${escapeHtml(loadK6Version)}</span>` : ''}</div>
@@ -7904,20 +8003,20 @@ function renderLoad(c) {
7904
8003
  <div class="load-config-hint">Если в скрипте есть <code>options.scenarios</code>, VibeRadar не ломает stages через CLI, а передаёт значения как <code>__ENV.LOAD_VUS</code>/<code>__ENV.LOAD_DURATION</code> и совместимые <code>SMOKE_*</code>/<code>AUTH_*</code>. Если scenarios нет, будут использованы CLI <code>--vus</code>/<code>--duration</code>.</div>
7905
8004
  <div class="load-config-row" style="margin-top:8px">
7906
8005
  <div class="load-config-field" style="flex:1;min-width:260px">
7907
- <label>Data dir / file <span style="color:var(--dim);font-weight:400">(для <code>open('./users.csv')</code> и локальных lib)</span></label>
7908
- <input id="loadDataDir" type="text" value="${escapeHtml(loadDataDirDraft)}" placeholder="load-tests/k6 или C:\\path\\to\\k6-data" />
8006
+ <label>Data dir / file <span style="color:var(--dim);font-weight:400">(только если скрипт использует <code>open('./file')</code> или локальные imports)</span></label>
8007
+ <input id="loadDataDir" type="text" value="${escapeHtml(loadDataDirDraft)}" placeholder="Оставь пустым, если файлы не нужны" />
7909
8008
  </div>
7910
8009
  <div class="load-config-field" style="flex:1;min-width:260px">
7911
- <label>Results dir <span style="color:var(--dim);font-weight:400">(необязательно)</span></label>
7912
- <input id="loadResultDir" type="text" value="${escapeHtml(loadResultDirDraft)}" placeholder=".viberadar/load-runs или C:\\path\\to\\results" />
8010
+ <label>Results dir <span style="color:var(--dim);font-weight:400">(куда сохранить summary/log/metrics; можно пустым)</span></label>
8011
+ <input id="loadResultDir" type="text" value="${escapeHtml(loadResultDirDraft)}" placeholder="Пусто = .viberadar/load-runs/<runId>/results" />
7913
8012
  </div>
7914
8013
  </div>
7915
- <div class="load-config-hint">Если указать Data dir, VibeRadar скопирует его содержимое рядом со скриптом перед запуском. Результаты сохраняются в <code>results/&lt;runId&gt;</code>: summary.json, metrics.ndjson, k6.log, config.json.</div>
8014
+ <div class="load-config-hint">Data dir нужен только для локальных файлов: например, если в скрипте есть <code>open('./users.csv')</code>, укажи папку, где лежит <code>users.csv</code>. Results dir можно не заполнять.</div>
7916
8015
  <div class="load-config-row" style="margin-top:8px">
7917
- <div class="load-config-field" style="flex:1;min-width:300px">
7918
- <label>Bearer Token <span style="color:var(--dim);font-weight:400;font-size:11px">(→ <code style="background:var(--bg2);padding:1px 4px;border-radius:3px">__ENV.TOKEN</code>)</span></label>
7919
- <input id="loadToken" type="password" placeholder="Вставь Bearer-токен (необязательно)" style="font-family:monospace;font-size:12px" oninput="localStorage.setItem('vr_load_token', this.value)" />
7920
- </div>
8016
+ <div class="load-config-field" style="flex:1;min-width:300px">
8017
+ <label>Accounts JSON <span style="color:var(--dim);font-weight:400">→ <code>__ENV.TEST_ACCOUNTS_JSON</code>, первая учётка также в <code>TEST_EMAIL/TEST_PASSWORD</code></span></label>
8018
+ <textarea class="load-accounts-json" id="loadAccountsJson" spellcheck="false" placeholder='[{"email":"user1@example.com","password":"pass1"},{"email":"user2@example.com","password":"pass2"}]'>${escapeHtml(loadAccountsJsonDraft)}</textarea>
8019
+ </div>
7921
8020
  </div>
7922
8021
  <div class="load-btns">
7923
8022
  <button class="load-btn load-btn-run" id="loadRunBtn" onclick="runLoadTest()" ${isRunning ? 'disabled' : ''}>▶ Запустить</button>
@@ -7974,10 +8073,6 @@ function renderLoad(c) {
7974
8073
  const logEl = document.getElementById('loadLogContent');
7975
8074
  if (logEl) logEl.scrollTop = logEl.scrollHeight;
7976
8075
 
7977
- // Restore token
7978
- const savedToken = localStorage.getItem('vr_load_token');
7979
- if (savedToken) { const el = document.getElementById('loadToken'); if (el) el.value = savedToken; }
7980
-
7981
8076
  // Sync textarea with draft
7982
8077
  const ta = document.getElementById('loadScriptEditor');
7983
8078
  if (ta) { ta.addEventListener('input', () => { loadScriptDraft = ta.value; }); }
@@ -7986,11 +8081,13 @@ function renderLoad(c) {
7986
8081
  const durationInput = document.getElementById('loadDuration');
7987
8082
  const dataInput = document.getElementById('loadDataDir');
7988
8083
  const resultInput = document.getElementById('loadResultDir');
8084
+ const accountsInput = document.getElementById('loadAccountsJson');
7989
8085
  if (baseInput) baseInput.addEventListener('input', () => { loadBaseUrlDraft = baseInput.value || 'http://localhost:5000'; });
7990
8086
  if (vusInput) vusInput.addEventListener('input', () => { loadVusDraft = parseInt(vusInput.value || '10', 10) || 10; });
7991
8087
  if (durationInput) durationInput.addEventListener('input', () => { loadDurationDraft = durationInput.value || '30s'; });
7992
8088
  if (dataInput) dataInput.addEventListener('input', () => { loadDataDirDraft = dataInput.value || ''; });
7993
8089
  if (resultInput) resultInput.addEventListener('input', () => { loadResultDirDraft = resultInput.value || ''; });
8090
+ if (accountsInput) accountsInput.addEventListener('input', () => { loadAccountsJsonDraft = accountsInput.value || ''; });
7994
8091
 
7995
8092
  // Draw charts if we have data
7996
8093
  if (loadBuckets && loadBuckets.length > 0) {
@@ -7998,18 +8095,20 @@ function renderLoad(c) {
7998
8095
  }
7999
8096
  }
8000
8097
 
8001
- function loadNewTest() {
8002
- loadScriptDraft = '';
8003
- loadScriptNameDraft = '';
8004
- loadView = 'editor';
8005
- renderContent();
8006
- }
8098
+ function loadNewTest() {
8099
+ loadScriptDraft = '';
8100
+ loadScriptNameDraft = '';
8101
+ loadImportedEnvVarsDraft = {};
8102
+ loadView = 'editor';
8103
+ renderContent();
8104
+ }
8007
8105
 
8008
8106
  function loadOpenScript(name) {
8009
8107
  const s = loadSavedScripts.find(x => x.name === name);
8010
8108
  if (!s) return;
8011
8109
  loadScriptDraft = s.script;
8012
8110
  loadScriptNameDraft = s.name;
8111
+ loadImportedEnvVarsDraft = {};
8013
8112
  if (s.baseUrl) loadBaseUrlDraft = s.baseUrl;
8014
8113
  if (s.vus) loadVusDraft = s.vus;
8015
8114
  if (s.duration) loadDurationDraft = s.duration;
@@ -8045,19 +8144,27 @@ async function runLoadTest() {
8045
8144
  const scriptName = (document.getElementById('loadScriptName')?.value || loadScriptNameDraft || 'Новый тест').trim();
8046
8145
  const dataDir = (document.getElementById('loadDataDir')?.value || loadDataDirDraft || '').trim();
8047
8146
  const resultDir = (document.getElementById('loadResultDir')?.value || loadResultDirDraft || '').trim();
8147
+ const accountsRaw = document.getElementById('loadAccountsJson')?.value || loadAccountsJsonDraft || '';
8148
+ const accountsInfo = normalizeAccountsJson(accountsRaw);
8149
+ if (accountsInfo.error) { alert(accountsInfo.error); return; }
8048
8150
  loadBaseUrlDraft = baseUrl;
8049
8151
  loadVusDraft = vus;
8050
8152
  loadDurationDraft = duration;
8051
8153
  loadScriptNameDraft = scriptName;
8052
8154
  loadDataDirDraft = dataDir;
8053
8155
  loadResultDirDraft = resultDir;
8156
+ loadAccountsJsonDraft = accountsRaw;
8054
8157
 
8055
8158
  loadLogLines = [];
8056
8159
  loadBuckets = [];
8057
8160
 
8058
- try {
8059
- const tokenVal = (document.getElementById('loadToken')?.value || localStorage.getItem('vr_load_token') || '').trim();
8060
- const envVars = tokenVal ? { TOKEN: tokenVal } : {};
8161
+ try {
8162
+ const envVars = { ...(loadImportedEnvVarsDraft || {}) };
8163
+ if (accountsInfo.accounts.length > 0) {
8164
+ envVars.TEST_ACCOUNTS_JSON = accountsInfo.text;
8165
+ envVars.TEST_EMAIL = accountsInfo.accounts[0].email;
8166
+ envVars.TEST_PASSWORD = accountsInfo.accounts[0].password;
8167
+ }
8061
8168
 
8062
8169
  const r = await fetch('/api/load/run', {
8063
8170
  method: 'POST',
@@ -8146,12 +8253,13 @@ ${featureList || '(нет данных)'}
8146
8253
  2. Можно использовать \`options.scenarios\`; тогда VibeRadar в Auto-режиме не будет передавать \`--vus/--duration\`, а прокинет значения UI в \`__ENV.SMOKE_VUS\` и \`__ENV.SMOKE_DURATION\`
8147
8254
  3. \`export default function() { ... }\` с проверками \`check()\`
8148
8255
  4. Base URL бери из \`__ENV.BASE_URL || '${baseUrl}'\`
8149
- 5. Если нужна авторизациядобавь заголовок Authorization (Bearer-токен как переменную __ENV.TOKEN)
8150
- 6. Если нужна загрузка файла — используй \`http.file()\` и \`open()\`
8151
- 7. Если нужны CSV/JSON/локальные helper-модули, используй относительные пути от Data dir: \`open('./users.csv')\`, \`import './lib/helper.js'\`
8152
- 8. Не добавляй \`handleSummary\` и не пиши результаты сам: VibeRadar сохраняет summary.json, metrics.ndjson, k6.log
8153
- 9. Добавь \`sleep(1)\` между запросами
8154
- 10. Добавь комментарий в начале: что тестируется и какой эндпоинт
8256
+ 5. Если нужны учёткичитай \`JSON.parse(__ENV.TEST_ACCOUNTS_JSON || '[]')\`, выбирай аккаунт по \`__VU\`; для старых скриптов доступна первая учётка как \`__ENV.TEST_EMAIL\`/\`__ENV.TEST_PASSWORD\`
8257
+ 6. Если нужна авторизация токеном в конкретном проекте — используй env-переменную с понятным именем, например \`__ENV.API_TOKEN\`
8258
+ 7. Если нужна загрузка файла используй \`http.file()\` и \`open()\`
8259
+ 8. Если нужны CSV/JSON/локальные helper-модули, используй относительные пути от Data dir: \`open('./users.csv')\`, \`import './lib/helper.js'\`
8260
+ 9. Не добавляй \`handleSummary\` и не пиши результаты сам: VibeRadar сохраняет summary.json, metrics.ndjson, k6.log
8261
+ 10. Добавь \`sleep(1)\` между запросами
8262
+ 11. Добавь комментарий в начале: что тестируется и какой эндпоинт
8155
8263
 
8156
8264
  **Шаг 3 — сохрани ТОЛЬКО скрипт в файл.**
8157
8265
  Запиши итоговый JavaScript-код в файл: \`.viberadar/load-script-generated.js\`
@@ -8246,6 +8354,7 @@ async function loadOpenRun(runId) {
8246
8354
  loadLogLines = d.logs || [];
8247
8355
  loadScriptDraft = d.script || '';
8248
8356
  loadScriptNameDraft = d.config?.scriptName || d.scriptName || '';
8357
+ loadImportedEnvVarsDraft = {};
8249
8358
  if (d.config) applyLoadConfigToFields(d.config);
8250
8359
  loadView = 'editor';
8251
8360
  renderContent();
@@ -8289,6 +8398,7 @@ async function loadLoadScript(name) {
8289
8398
  if (!s) return;
8290
8399
  loadScriptDraft = s.script;
8291
8400
  loadScriptNameDraft = s.name;
8401
+ loadImportedEnvVarsDraft = {};
8292
8402
  if (s.baseUrl) loadBaseUrlDraft = s.baseUrl;
8293
8403
  if (s.vus) loadVusDraft = s.vus;
8294
8404
  if (s.duration) loadDurationDraft = s.duration;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.213",
3
+ "version": "0.3.215",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {