viberadar 0.3.164 → 0.3.166

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.
@@ -1485,6 +1485,39 @@
1485
1485
  color: var(--text); font-size: 12px; padding: 5px 9px; outline: none;
1486
1486
  }
1487
1487
  .load-save-input:focus { border-color: var(--blue); }
1488
+ /* Library view */
1489
+ .load-library-header {
1490
+ display: flex; align-items: center; justify-content: space-between;
1491
+ padding: 16px 20px 12px; border-bottom: 1px solid var(--border); margin-bottom: 16px;
1492
+ }
1493
+ .load-library-grid {
1494
+ display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr));
1495
+ gap: 12px; padding: 0 20px 20px;
1496
+ }
1497
+ .load-library-card {
1498
+ background: var(--bg); border: 1px solid var(--border); border-radius: 8px;
1499
+ padding: 14px 16px; cursor: pointer; transition: border-color 0.15s, background 0.15s;
1500
+ }
1501
+ .load-library-card:hover { border-color: var(--blue); background: #1a2a3a22; }
1502
+ .load-library-card-name { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 4px; }
1503
+ .load-library-card-meta { font-size: 11px; color: var(--muted); margin-bottom: 10px; }
1504
+ .load-library-card-actions { display: flex; gap: 6px; }
1505
+ .load-library-empty {
1506
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
1507
+ padding: 60px 20px; text-align: center; color: var(--dim);
1508
+ }
1509
+ /* Editor topbar */
1510
+ .load-editor-topbar {
1511
+ display: flex; align-items: center; gap: 10px;
1512
+ padding: 10px 20px 6px; border-bottom: 1px solid var(--border); margin-bottom: 4px;
1513
+ }
1514
+ .load-back-btn {
1515
+ background: none; border: 1px solid var(--border); color: var(--dim);
1516
+ border-radius: 5px; padding: 4px 12px; font-size: 12px; cursor: pointer;
1517
+ transition: border-color 0.15s, color 0.15s;
1518
+ }
1519
+ .load-back-btn:hover { border-color: var(--fg); color: var(--text); }
1520
+ /* Legacy list (kept for compatibility) */
1488
1521
  .load-scripts-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
1489
1522
  .load-script-item {
1490
1523
  display: flex; align-items: center; gap: 8px; padding: 8px 10px;
@@ -1664,6 +1697,7 @@ let loadAiPromptDraft = ''; // AI prompt textarea draft
1664
1697
  let loadAiGenerating = false; // waiting for agent to produce script
1665
1698
  let loadSavedScripts = []; // [{ name, date, script }]
1666
1699
  let loadScriptNameDraft = ''; // save name input draft
1700
+ let loadView = 'library'; // 'library' | 'editor'
1667
1701
 
1668
1702
  function toggleObsHint(id) {
1669
1703
  document.getElementById(id).classList.toggle('open');
@@ -1753,11 +1787,9 @@ function switchMode(nextMode) {
1753
1787
  if (contextMode === 'load' && loadK6Available === null) { checkK6(); }
1754
1788
  if (contextMode === 'load') {
1755
1789
  loadRefreshScripts();
1756
- // restore saved token after render
1757
- requestAnimationFrame(() => {
1758
- const saved = localStorage.getItem('vr_load_token');
1759
- if (saved) { const el = document.getElementById('loadToken'); if (el) el.value = saved; }
1760
- });
1790
+ // reset to library view when switching to load tab (unless test is running)
1791
+ const isRunning = loadState && loadState.status === 'running';
1792
+ if (!isRunning) loadView = 'library';
1761
1793
  }
1762
1794
  setModeRoute(contextMode);
1763
1795
  document.getElementById('searchInput').value = searchQuery;
@@ -6766,7 +6798,7 @@ async function checkK6() {
6766
6798
  }
6767
6799
 
6768
6800
  function buildDefaultScript(cfg) {
6769
- const baseUrl = cfg.baseUrl || 'http://localhost:3000';
6801
+ const baseUrl = cfg.baseUrl || 'http://localhost:5000';
6770
6802
  const endpoints = (cfg.endpoints || ['/']).filter(Boolean);
6771
6803
  const endpointLines = endpoints.map(ep =>
6772
6804
  ` const r = http.get(\`\${BASE_URL}${ep}\`);\n check(r, { 'status 2xx': (res) => res.status >= 200 && res.status < 300 });`
@@ -6800,7 +6832,7 @@ function generateScriptFromFeature(featureKey) {
6800
6832
  const apiMods = mods.filter(m => m.type === 'service' || (m.name && /api|service|controller|handler/i.test(m.name)));
6801
6833
  const endpoints = apiMods.slice(0, 5).map(m => '/' + m.name.replace(/\.(ts|js)$/, '').replace(/\\/g, '/'));
6802
6834
  return buildDefaultScript({
6803
- baseUrl: 'http://localhost:3000',
6835
+ baseUrl: 'http://localhost:5000',
6804
6836
  vus: 10,
6805
6837
  duration: '30s',
6806
6838
  endpoints: endpoints.length ? endpoints : ['/'],
@@ -6900,7 +6932,7 @@ function renderLoad(c) {
6900
6932
  const isDone = loadState && (loadState.status === 'done' || loadState.status === 'stopped');
6901
6933
  const hasErr = loadState && loadState.status === 'error';
6902
6934
 
6903
- // k6 install check message
6935
+ // k6 not installed
6904
6936
  if (loadK6Available === false) {
6905
6937
  c.innerHTML = `<div class="load-screen"><div class="load-no-k6">
6906
6938
  <h3>⚠ k6 не найден</h3>
@@ -6914,20 +6946,57 @@ function renderLoad(c) {
6914
6946
  </div></div>`;
6915
6947
  return;
6916
6948
  }
6917
-
6918
6949
  if (loadK6Available === null) {
6919
6950
  c.innerHTML = `<div class="load-screen" style="padding:40px;text-align:center;color:var(--muted)">Проверяю k6…</div>`;
6920
6951
  return;
6921
6952
  }
6922
6953
 
6954
+ // If test is running — force editor view
6955
+ const view = isRunning ? 'editor' : loadView;
6956
+
6957
+ // ─── LIBRARY VIEW ─────────────────────────────────────────────────────────
6958
+ if (view === 'library') {
6959
+ const cards = loadSavedScripts.map(s => `
6960
+ <div class="load-library-card" data-sname="${escapeHtml(s.name)}">
6961
+ <div class="load-library-card-name">${escapeHtml(s.name)}</div>
6962
+ <div class="load-library-card-meta">${escapeHtml(s.date)}</div>
6963
+ </div>`).join('');
6964
+
6965
+ c.innerHTML = `<div class="load-screen">
6966
+ <div class="load-library-header">
6967
+ <div>
6968
+ <div style="font-size:16px;font-weight:600;color:var(--fg)">Нагрузочные тесты</div>
6969
+ <div style="font-size:12px;color:var(--muted);margin-top:2px">${loadK6Version ? escapeHtml(loadK6Version) : ''} · ${loadSavedScripts.length} сохранённых</div>
6970
+ </div>
6971
+ <button class="load-btn load-btn-run" style="font-size:13px;padding:8px 20px" onclick="loadNewTest()">+ Новый тест</button>
6972
+ </div>
6973
+
6974
+ ${loadSavedScripts.length === 0
6975
+ ? `<div class="load-library-empty">
6976
+ <div style="font-size:32px;margin-bottom:12px">📋</div>
6977
+ <div style="font-size:14px;font-weight:500;margin-bottom:6px">Нет сохранённых тестов</div>
6978
+ <div style="font-size:12px;color:var(--muted);margin-bottom:16px">Создай первый тест — напиши k6 скрипт или сгенерируй через AI</div>
6979
+ <button class="load-btn load-btn-run" onclick="loadNewTest()">+ Создать первый тест</button>
6980
+ </div>`
6981
+ : `<div class="load-library-grid">${cards}</div>`
6982
+ }
6983
+ </div>`;
6984
+
6985
+ // Attach click handlers via data attribute — avoids quoting issues
6986
+ c.querySelectorAll('.load-library-card[data-sname]').forEach(el => {
6987
+ el.addEventListener('click', () => loadOpenScript(el.dataset.sname));
6988
+ });
6989
+ return;
6990
+ }
6991
+
6992
+ // ─── EDITOR VIEW ──────────────────────────────────────────────────────────
6923
6993
  const features = (D && D.features) || [];
6924
6994
  const featureOptions = features.map(f =>
6925
6995
  `<option value="${f.key}">${f.label || f.key}</option>`
6926
6996
  ).join('');
6927
6997
 
6928
- // Build script draft if empty
6929
6998
  if (!loadScriptDraft) {
6930
- loadScriptDraft = buildDefaultScript({ baseUrl: 'http://localhost:3000', vus: 10, duration: '30s', endpoints: ['/'] });
6999
+ loadScriptDraft = buildDefaultScript({ baseUrl: 'http://localhost:5000', vus: 10, duration: '30s', endpoints: ['/'] });
6931
7000
  }
6932
7001
 
6933
7002
  const statusBadge = !loadState || loadState.status === 'idle' ? '' :
@@ -6941,12 +7010,17 @@ function renderLoad(c) {
6941
7010
 
6942
7011
  c.innerHTML = `<div class="load-screen">
6943
7012
 
7013
+ <div class="load-editor-topbar">
7014
+ ${!isRunning ? `<button class="load-back-btn" onclick="loadView='library';renderContent()">← Все тесты</button>` : ''}
7015
+ <span style="font-size:12px;color:var(--muted);margin-left:4px">${statusBadge}</span>
7016
+ </div>
7017
+
6944
7018
  <div class="load-section">
6945
- <div class="load-section-title">Конфигурация ${loadK6Version ? `<span style="color:var(--dim);font-weight:400">${escapeHtml(loadK6Version)}</span>` : ''} ${statusBadge}</div>
7019
+ <div class="load-section-title">Конфигурация ${loadK6Version ? `<span style="color:var(--dim);font-weight:400">${escapeHtml(loadK6Version)}</span>` : ''}</div>
6946
7020
  <div class="load-config-row">
6947
7021
  <div class="load-config-field" style="flex:1;min-width:200px">
6948
7022
  <label>Base URL</label>
6949
- <input id="loadBaseUrl" type="text" value="http://localhost:3000" placeholder="http://localhost:3000" />
7023
+ <input id="loadBaseUrl" type="text" value="http://localhost:5000" placeholder="http://localhost:5000" />
6950
7024
  </div>
6951
7025
  <div class="load-config-field">
6952
7026
  <label>VUs</label>
@@ -6959,7 +7033,7 @@ function renderLoad(c) {
6959
7033
  </div>
6960
7034
  <div class="load-config-row" style="margin-top:8px">
6961
7035
  <div class="load-config-field" style="flex:1;min-width:300px">
6962
- <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>
7036
+ <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>
6963
7037
  <input id="loadToken" type="password" placeholder="Вставь Bearer-токен (необязательно)" style="font-family:monospace;font-size:12px" oninput="localStorage.setItem('vr_load_token', this.value)" />
6964
7038
  </div>
6965
7039
  ${featureOptions ? `<div class="load-config-field">
@@ -6979,9 +7053,9 @@ function renderLoad(c) {
6979
7053
 
6980
7054
  <div class="load-section">
6981
7055
  <div class="load-section-title">🤖 AI генерация скрипта</div>
6982
- <div style="font-size:11px;color:var(--muted);margin-bottom:6px">Опиши сценарий — агент сгенерирует k6-скрипт и вставит его в редактор</div>
7056
+ <div style="font-size:11px;color:var(--muted);margin-bottom:6px">Опиши сценарий — агент найдёт эндпоинты в коде и сгенерирует k6-скрипт</div>
6983
7057
  <div class="load-ai-prompt-wrap">
6984
- <textarea class="load-ai-prompt" id="loadAiPrompt" placeholder="Например: загрузка аудио .mp3 на /api/transcribe с полем language=ru, нужна авторизация Bearer-токеном, тестировать 20 VU 1 минуту&#10;&#10;Или: чат с RAG — POST /api/chat с body {message, knowledge_base_id, workspace_id}, stream:false, проверить что ответ содержит поле answer">${escapeHtml(loadAiPromptDraft)}</textarea>
7058
+ <textarea class="load-ai-prompt" id="loadAiPrompt" placeholder="Например: загрузка аудио .mp3 на /api/transcribe с полем language=ru, нужна авторизация Bearer-токеном, тестировать 20 VU 1 минуту&#10;&#10;Или: чат с RAG — POST /api/chat с body {message, knowledge_base_id, workspace_id}, stream:false">${escapeHtml(loadAiPromptDraft)}</textarea>
6985
7059
  <button class="load-btn load-btn-ai" id="loadAiGenBtn" onclick="loadAiGenerateScript()" ${agentRunning ? 'disabled' : ''}>🤖 Сгенерировать<br>через AI</button>
6986
7060
  </div>
6987
7061
  <div class="load-ai-status" id="loadAiStatus">${loadAiGenerating ? '⏳ Агент генерирует скрипт… следи в терминале ↓' : ''}</div>
@@ -6991,31 +7065,9 @@ function renderLoad(c) {
6991
7065
  <div class="load-section-title">k6 скрипт <span style="font-weight:400;color:var(--dim)">(редактируемый)</span></div>
6992
7066
  <textarea class="load-script-editor" id="loadScriptEditor" spellcheck="false">${escapeHtml(loadScriptDraft)}</textarea>
6993
7067
  <div class="load-save-row">
6994
- <input class="load-save-input" id="loadScriptName" placeholder="Название скрипта, например: transcribe-audio-20vu" value="${escapeHtml(loadScriptNameDraft)}" />
6995
- <button class="load-btn" style="white-space:nowrap" onclick="loadSaveScript()">💾 Сохранить скрипт</button>
6996
- </div>
6997
- </div>
6998
-
6999
- <div class="load-section">
7000
- <div class="load-section-title" style="display:flex;align-items:center;gap:8px">
7001
- 📁 Сохранённые скрипты
7002
- <span style="font-size:11px;font-weight:400;color:var(--muted)">${loadSavedScripts.length} шт.</span>
7003
- <button class="load-btn" style="margin-left:auto;padding:2px 8px;font-size:11px" onclick="loadRefreshScripts()">↻ обновить</button>
7068
+ <input class="load-save-input" id="loadScriptName" placeholder="Название, например: transcribe-audio-20vu" value="${escapeHtml(loadScriptNameDraft)}" />
7069
+ <button class="load-btn" style="white-space:nowrap" onclick="loadSaveScript()">💾 Сохранить</button>
7004
7070
  </div>
7005
- ${loadSavedScripts.length === 0
7006
- ? `<div style="font-size:12px;color:var(--muted);padding:8px 0">Нет сохранённых скриптов — напиши скрипт выше и нажми «💾 Сохранить»</div>`
7007
- : `<div class="load-scripts-list">
7008
- ${loadSavedScripts.map(s => `
7009
- <div class="load-script-item">
7010
- <div class="load-script-item-name">${escapeHtml(s.name)}</div>
7011
- <div class="load-script-item-meta">${escapeHtml(s.date)}</div>
7012
- <div class="load-script-item-actions">
7013
- <button class="load-script-load-btn" onclick="loadLoadScript(${JSON.stringify(s.name)})">▶ Загрузить</button>
7014
- <button class="load-script-del-btn" onclick="loadDeleteScript(${JSON.stringify(s.name)})">✕</button>
7015
- </div>
7016
- </div>`).join('')}
7017
- </div>`
7018
- }
7019
7071
  </div>
7020
7072
 
7021
7073
  ${(isRunning || isDone || hasErr) ? `<div class="load-section">
@@ -7056,11 +7108,13 @@ function renderLoad(c) {
7056
7108
  const logEl = document.getElementById('loadLogContent');
7057
7109
  if (logEl) logEl.scrollTop = logEl.scrollHeight;
7058
7110
 
7111
+ // Restore token
7112
+ const savedToken = localStorage.getItem('vr_load_token');
7113
+ if (savedToken) { const el = document.getElementById('loadToken'); if (el) el.value = savedToken; }
7114
+
7059
7115
  // Sync textarea with draft
7060
7116
  const ta = document.getElementById('loadScriptEditor');
7061
- if (ta) {
7062
- ta.addEventListener('input', () => { loadScriptDraft = ta.value; });
7063
- }
7117
+ if (ta) { ta.addEventListener('input', () => { loadScriptDraft = ta.value; }); }
7064
7118
 
7065
7119
  // Feature selector auto-generates script
7066
7120
  const featSel = document.getElementById('loadFeature');
@@ -7083,8 +7137,24 @@ function renderLoad(c) {
7083
7137
  }
7084
7138
  }
7085
7139
 
7140
+ function loadNewTest() {
7141
+ loadScriptDraft = '';
7142
+ loadScriptNameDraft = '';
7143
+ loadView = 'editor';
7144
+ renderContent();
7145
+ }
7146
+
7147
+ function loadOpenScript(name) {
7148
+ const s = loadSavedScripts.find(x => x.name === name);
7149
+ if (!s) return;
7150
+ loadScriptDraft = s.script;
7151
+ loadScriptNameDraft = s.name;
7152
+ loadView = 'editor';
7153
+ renderContent();
7154
+ }
7155
+
7086
7156
  function loadGenerateScript() {
7087
- const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:3000';
7157
+ const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:5000';
7088
7158
  const vus = parseInt(document.getElementById('loadVus')?.value || '10');
7089
7159
  const duration = document.getElementById('loadDuration')?.value || '30s';
7090
7160
  const featKey = document.getElementById('loadFeature')?.value || '';
@@ -7165,7 +7235,7 @@ async function loadAiGenerateScript() {
7165
7235
  const userPrompt = promptEl ? promptEl.value.trim() : loadAiPromptDraft.trim();
7166
7236
  if (!userPrompt) { alert('Опиши сценарий нагрузочного теста в поле выше'); return; }
7167
7237
 
7168
- const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:3000';
7238
+ const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:5000';
7169
7239
  const vus = document.getElementById('loadVus')?.value || '10';
7170
7240
  const duration = document.getElementById('loadDuration')?.value || '30s';
7171
7241
 
@@ -7257,7 +7327,8 @@ async function loadRefreshScripts() {
7257
7327
  const r = await fetch('/api/load/scripts');
7258
7328
  if (!r.ok) return;
7259
7329
  loadSavedScripts = await r.json();
7260
- renderContent();
7330
+ // Only re-render if we're on load tab in library view (don't disrupt editor)
7331
+ if (contextMode === 'load' && loadView === 'library') renderContent();
7261
7332
  } catch {}
7262
7333
  }
7263
7334
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.164",
3
+ "version": "0.3.166",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {