viberadar 0.3.164 → 0.3.165

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,56 @@ 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" onclick="loadOpenScript(${JSON.stringify(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 class="load-library-card-actions">
6964
+ <button class="load-script-load-btn" onclick="event.stopPropagation();loadOpenScript(${JSON.stringify(s.name)})">▶ Открыть</button>
6965
+ <button class="load-script-del-btn" onclick="event.stopPropagation();loadDeleteScript(${JSON.stringify(s.name)})">✕</button>
6966
+ </div>
6967
+ </div>`).join('');
6968
+
6969
+ c.innerHTML = `<div class="load-screen">
6970
+ <div class="load-library-header">
6971
+ <div>
6972
+ <div style="font-size:16px;font-weight:600;color:var(--fg)">Нагрузочные тесты</div>
6973
+ <div style="font-size:12px;color:var(--muted);margin-top:2px">${loadK6Version ? escapeHtml(loadK6Version) : ''} · ${loadSavedScripts.length} сохранённых</div>
6974
+ </div>
6975
+ <button class="load-btn load-btn-run" style="font-size:13px;padding:8px 20px" onclick="loadNewTest()">+ Новый тест</button>
6976
+ </div>
6977
+
6978
+ ${loadSavedScripts.length === 0
6979
+ ? `<div class="load-library-empty">
6980
+ <div style="font-size:32px;margin-bottom:12px">📋</div>
6981
+ <div style="font-size:14px;font-weight:500;margin-bottom:6px">Нет сохранённых тестов</div>
6982
+ <div style="font-size:12px;color:var(--muted);margin-bottom:16px">Создай первый тест — напиши k6 скрипт или сгенерируй через AI</div>
6983
+ <button class="load-btn load-btn-run" onclick="loadNewTest()">+ Создать первый тест</button>
6984
+ </div>`
6985
+ : `<div class="load-library-grid">${cards}</div>`
6986
+ }
6987
+ </div>`;
6988
+ return;
6989
+ }
6990
+
6991
+ // ─── EDITOR VIEW ──────────────────────────────────────────────────────────
6923
6992
  const features = (D && D.features) || [];
6924
6993
  const featureOptions = features.map(f =>
6925
6994
  `<option value="${f.key}">${f.label || f.key}</option>`
6926
6995
  ).join('');
6927
6996
 
6928
- // Build script draft if empty
6929
6997
  if (!loadScriptDraft) {
6930
- loadScriptDraft = buildDefaultScript({ baseUrl: 'http://localhost:3000', vus: 10, duration: '30s', endpoints: ['/'] });
6998
+ loadScriptDraft = buildDefaultScript({ baseUrl: 'http://localhost:5000', vus: 10, duration: '30s', endpoints: ['/'] });
6931
6999
  }
6932
7000
 
6933
7001
  const statusBadge = !loadState || loadState.status === 'idle' ? '' :
@@ -6941,12 +7009,17 @@ function renderLoad(c) {
6941
7009
 
6942
7010
  c.innerHTML = `<div class="load-screen">
6943
7011
 
7012
+ <div class="load-editor-topbar">
7013
+ ${!isRunning ? `<button class="load-back-btn" onclick="loadView='library';renderContent()">← Все тесты</button>` : ''}
7014
+ <span style="font-size:12px;color:var(--muted);margin-left:4px">${statusBadge}</span>
7015
+ </div>
7016
+
6944
7017
  <div class="load-section">
6945
- <div class="load-section-title">Конфигурация ${loadK6Version ? `<span style="color:var(--dim);font-weight:400">${escapeHtml(loadK6Version)}</span>` : ''} ${statusBadge}</div>
7018
+ <div class="load-section-title">Конфигурация ${loadK6Version ? `<span style="color:var(--dim);font-weight:400">${escapeHtml(loadK6Version)}</span>` : ''}</div>
6946
7019
  <div class="load-config-row">
6947
7020
  <div class="load-config-field" style="flex:1;min-width:200px">
6948
7021
  <label>Base URL</label>
6949
- <input id="loadBaseUrl" type="text" value="http://localhost:3000" placeholder="http://localhost:3000" />
7022
+ <input id="loadBaseUrl" type="text" value="http://localhost:5000" placeholder="http://localhost:5000" />
6950
7023
  </div>
6951
7024
  <div class="load-config-field">
6952
7025
  <label>VUs</label>
@@ -6959,7 +7032,7 @@ function renderLoad(c) {
6959
7032
  </div>
6960
7033
  <div class="load-config-row" style="margin-top:8px">
6961
7034
  <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>
7035
+ <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
7036
  <input id="loadToken" type="password" placeholder="Вставь Bearer-токен (необязательно)" style="font-family:monospace;font-size:12px" oninput="localStorage.setItem('vr_load_token', this.value)" />
6964
7037
  </div>
6965
7038
  ${featureOptions ? `<div class="load-config-field">
@@ -6979,9 +7052,9 @@ function renderLoad(c) {
6979
7052
 
6980
7053
  <div class="load-section">
6981
7054
  <div class="load-section-title">🤖 AI генерация скрипта</div>
6982
- <div style="font-size:11px;color:var(--muted);margin-bottom:6px">Опиши сценарий — агент сгенерирует k6-скрипт и вставит его в редактор</div>
7055
+ <div style="font-size:11px;color:var(--muted);margin-bottom:6px">Опиши сценарий — агент найдёт эндпоинты в коде и сгенерирует k6-скрипт</div>
6983
7056
  <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>
7057
+ <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
7058
  <button class="load-btn load-btn-ai" id="loadAiGenBtn" onclick="loadAiGenerateScript()" ${agentRunning ? 'disabled' : ''}>🤖 Сгенерировать<br>через AI</button>
6986
7059
  </div>
6987
7060
  <div class="load-ai-status" id="loadAiStatus">${loadAiGenerating ? '⏳ Агент генерирует скрипт… следи в терминале ↓' : ''}</div>
@@ -6991,31 +7064,9 @@ function renderLoad(c) {
6991
7064
  <div class="load-section-title">k6 скрипт <span style="font-weight:400;color:var(--dim)">(редактируемый)</span></div>
6992
7065
  <textarea class="load-script-editor" id="loadScriptEditor" spellcheck="false">${escapeHtml(loadScriptDraft)}</textarea>
6993
7066
  <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>
7067
+ <input class="load-save-input" id="loadScriptName" placeholder="Название, например: transcribe-audio-20vu" value="${escapeHtml(loadScriptNameDraft)}" />
7068
+ <button class="load-btn" style="white-space:nowrap" onclick="loadSaveScript()">💾 Сохранить</button>
7004
7069
  </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
7070
  </div>
7020
7071
 
7021
7072
  ${(isRunning || isDone || hasErr) ? `<div class="load-section">
@@ -7056,11 +7107,13 @@ function renderLoad(c) {
7056
7107
  const logEl = document.getElementById('loadLogContent');
7057
7108
  if (logEl) logEl.scrollTop = logEl.scrollHeight;
7058
7109
 
7110
+ // Restore token
7111
+ const savedToken = localStorage.getItem('vr_load_token');
7112
+ if (savedToken) { const el = document.getElementById('loadToken'); if (el) el.value = savedToken; }
7113
+
7059
7114
  // Sync textarea with draft
7060
7115
  const ta = document.getElementById('loadScriptEditor');
7061
- if (ta) {
7062
- ta.addEventListener('input', () => { loadScriptDraft = ta.value; });
7063
- }
7116
+ if (ta) { ta.addEventListener('input', () => { loadScriptDraft = ta.value; }); }
7064
7117
 
7065
7118
  // Feature selector auto-generates script
7066
7119
  const featSel = document.getElementById('loadFeature');
@@ -7083,8 +7136,24 @@ function renderLoad(c) {
7083
7136
  }
7084
7137
  }
7085
7138
 
7139
+ function loadNewTest() {
7140
+ loadScriptDraft = '';
7141
+ loadScriptNameDraft = '';
7142
+ loadView = 'editor';
7143
+ renderContent();
7144
+ }
7145
+
7146
+ function loadOpenScript(name) {
7147
+ const s = loadSavedScripts.find(x => x.name === name);
7148
+ if (!s) return;
7149
+ loadScriptDraft = s.script;
7150
+ loadScriptNameDraft = s.name;
7151
+ loadView = 'editor';
7152
+ renderContent();
7153
+ }
7154
+
7086
7155
  function loadGenerateScript() {
7087
- const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:3000';
7156
+ const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:5000';
7088
7157
  const vus = parseInt(document.getElementById('loadVus')?.value || '10');
7089
7158
  const duration = document.getElementById('loadDuration')?.value || '30s';
7090
7159
  const featKey = document.getElementById('loadFeature')?.value || '';
@@ -7165,7 +7234,7 @@ async function loadAiGenerateScript() {
7165
7234
  const userPrompt = promptEl ? promptEl.value.trim() : loadAiPromptDraft.trim();
7166
7235
  if (!userPrompt) { alert('Опиши сценарий нагрузочного теста в поле выше'); return; }
7167
7236
 
7168
- const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:3000';
7237
+ const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:5000';
7169
7238
  const vus = document.getElementById('loadVus')?.value || '10';
7170
7239
  const duration = document.getElementById('loadDuration')?.value || '30s';
7171
7240
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.164",
3
+ "version": "0.3.165",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {