viberadar 0.3.160 → 0.3.162
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 +103 -0
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +223 -0
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -1468,6 +1468,43 @@
|
|
|
1468
1468
|
.load-btn-run:hover:not(:disabled) { background: #2a4a2a; }
|
|
1469
1469
|
.load-btn-stop { background: #3a1a1a; border-color: var(--red); color: var(--red); }
|
|
1470
1470
|
.load-btn-stop:hover:not(:disabled) { background: #4a2a2a; }
|
|
1471
|
+
.load-ai-prompt-wrap { display: flex; gap: 8px; align-items: flex-start; margin-top: 10px; }
|
|
1472
|
+
.load-ai-prompt {
|
|
1473
|
+
flex: 1; min-height: 60px; background: var(--bg); border: 1px solid var(--border);
|
|
1474
|
+
border-radius: 5px; color: var(--text); font-family: inherit; font-size: 12px;
|
|
1475
|
+
padding: 8px 10px; resize: vertical; outline: none; line-height: 1.5;
|
|
1476
|
+
}
|
|
1477
|
+
.load-ai-prompt:focus { border-color: #8b5cf6; }
|
|
1478
|
+
.load-btn-ai { background: #1e1a3a; border-color: #8b5cf6; color: #a78bfa; white-space: nowrap; }
|
|
1479
|
+
.load-btn-ai:hover:not(:disabled) { background: #2a2050; }
|
|
1480
|
+
.load-btn-ai:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1481
|
+
.load-ai-status { font-size: 11px; color: #a78bfa; margin-top: 5px; min-height: 16px; }
|
|
1482
|
+
.load-save-row { display: flex; gap: 8px; align-items: center; margin-top: 10px; }
|
|
1483
|
+
.load-save-input {
|
|
1484
|
+
flex: 1; background: var(--bg); border: 1px solid var(--border); border-radius: 5px;
|
|
1485
|
+
color: var(--text); font-size: 12px; padding: 5px 9px; outline: none;
|
|
1486
|
+
}
|
|
1487
|
+
.load-save-input:focus { border-color: var(--blue); }
|
|
1488
|
+
.load-scripts-list { display: flex; flex-direction: column; gap: 6px; margin-top: 10px; }
|
|
1489
|
+
.load-script-item {
|
|
1490
|
+
display: flex; align-items: center; gap: 8px; padding: 8px 10px;
|
|
1491
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
1492
|
+
transition: border-color 0.15s;
|
|
1493
|
+
}
|
|
1494
|
+
.load-script-item:hover { border-color: var(--dim); }
|
|
1495
|
+
.load-script-item-name { flex: 1; font-size: 13px; color: var(--text); font-weight: 500; }
|
|
1496
|
+
.load-script-item-meta { font-size: 11px; color: var(--muted); white-space: nowrap; }
|
|
1497
|
+
.load-script-item-actions { display: flex; gap: 5px; }
|
|
1498
|
+
.load-script-load-btn {
|
|
1499
|
+
padding: 3px 10px; border-radius: 4px; border: 1px solid var(--blue);
|
|
1500
|
+
background: #1a2a3a; color: var(--blue); font-size: 11px; cursor: pointer;
|
|
1501
|
+
}
|
|
1502
|
+
.load-script-load-btn:hover { background: #253a4a; }
|
|
1503
|
+
.load-script-del-btn {
|
|
1504
|
+
padding: 3px 8px; border-radius: 4px; border: 1px solid var(--border);
|
|
1505
|
+
background: transparent; color: var(--muted); font-size: 11px; cursor: pointer;
|
|
1506
|
+
}
|
|
1507
|
+
.load-script-del-btn:hover { border-color: var(--red); color: var(--red); }
|
|
1471
1508
|
.load-charts { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
|
|
1472
1509
|
.load-chart-box { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 10px; }
|
|
1473
1510
|
.load-chart-label { font-size: 11px; color: var(--muted); margin-bottom: 6px; }
|
|
@@ -1623,6 +1660,10 @@ let loadLogLines = []; // log buffer for display
|
|
|
1623
1660
|
let loadK6Available = null; // null = unchecked, true/false
|
|
1624
1661
|
let loadK6Version = '';
|
|
1625
1662
|
let loadScriptDraft = ''; // editable k6 script
|
|
1663
|
+
let loadAiPromptDraft = ''; // AI prompt textarea draft
|
|
1664
|
+
let loadAiGenerating = false; // waiting for agent to produce script
|
|
1665
|
+
let loadSavedScripts = []; // [{ name, date, script }]
|
|
1666
|
+
let loadScriptNameDraft = ''; // save name input draft
|
|
1626
1667
|
|
|
1627
1668
|
function toggleObsHint(id) {
|
|
1628
1669
|
document.getElementById(id).classList.toggle('open');
|
|
@@ -1710,6 +1751,7 @@ function switchMode(nextMode) {
|
|
|
1710
1751
|
clearFeatureHash();
|
|
1711
1752
|
}
|
|
1712
1753
|
if (contextMode === 'load' && loadK6Available === null) { checkK6(); }
|
|
1754
|
+
if (contextMode === 'load') { loadRefreshScripts(); }
|
|
1713
1755
|
setModeRoute(contextMode);
|
|
1714
1756
|
document.getElementById('searchInput').value = searchQuery;
|
|
1715
1757
|
document.getElementById('panel').classList.remove('open');
|
|
@@ -6917,9 +6959,45 @@ function renderLoad(c) {
|
|
|
6917
6959
|
</div>
|
|
6918
6960
|
</div>
|
|
6919
6961
|
|
|
6962
|
+
<div class="load-section">
|
|
6963
|
+
<div class="load-section-title">🤖 AI генерация скрипта</div>
|
|
6964
|
+
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Опиши сценарий — агент сгенерирует k6-скрипт и вставит его в редактор</div>
|
|
6965
|
+
<div class="load-ai-prompt-wrap">
|
|
6966
|
+
<textarea class="load-ai-prompt" id="loadAiPrompt" placeholder="Например: загрузка аудио .mp3 на /api/transcribe с полем language=ru, нужна авторизация Bearer-токеном, тестировать 20 VU 1 минуту Или: чат с RAG — POST /api/chat с body {message, knowledge_base_id, workspace_id}, stream:false, проверить что ответ содержит поле answer">${escapeHtml(loadAiPromptDraft)}</textarea>
|
|
6967
|
+
<button class="load-btn load-btn-ai" id="loadAiGenBtn" onclick="loadAiGenerateScript()" ${agentRunning ? 'disabled' : ''}>🤖 Сгенерировать<br>через AI</button>
|
|
6968
|
+
</div>
|
|
6969
|
+
<div class="load-ai-status" id="loadAiStatus">${loadAiGenerating ? '⏳ Агент генерирует скрипт… следи в терминале ↓' : ''}</div>
|
|
6970
|
+
</div>
|
|
6971
|
+
|
|
6920
6972
|
<div class="load-section">
|
|
6921
6973
|
<div class="load-section-title">k6 скрипт <span style="font-weight:400;color:var(--dim)">(редактируемый)</span></div>
|
|
6922
6974
|
<textarea class="load-script-editor" id="loadScriptEditor" spellcheck="false">${escapeHtml(loadScriptDraft)}</textarea>
|
|
6975
|
+
<div class="load-save-row">
|
|
6976
|
+
<input class="load-save-input" id="loadScriptName" placeholder="Название скрипта, например: transcribe-audio-20vu" value="${escapeHtml(loadScriptNameDraft)}" />
|
|
6977
|
+
<button class="load-btn" style="white-space:nowrap" onclick="loadSaveScript()">💾 Сохранить скрипт</button>
|
|
6978
|
+
</div>
|
|
6979
|
+
</div>
|
|
6980
|
+
|
|
6981
|
+
<div class="load-section">
|
|
6982
|
+
<div class="load-section-title" style="display:flex;align-items:center;gap:8px">
|
|
6983
|
+
📁 Сохранённые скрипты
|
|
6984
|
+
<span style="font-size:11px;font-weight:400;color:var(--muted)">${loadSavedScripts.length} шт.</span>
|
|
6985
|
+
<button class="load-btn" style="margin-left:auto;padding:2px 8px;font-size:11px" onclick="loadRefreshScripts()">↻ обновить</button>
|
|
6986
|
+
</div>
|
|
6987
|
+
${loadSavedScripts.length === 0
|
|
6988
|
+
? `<div style="font-size:12px;color:var(--muted);padding:8px 0">Нет сохранённых скриптов — напиши скрипт выше и нажми «💾 Сохранить»</div>`
|
|
6989
|
+
: `<div class="load-scripts-list">
|
|
6990
|
+
${loadSavedScripts.map(s => `
|
|
6991
|
+
<div class="load-script-item">
|
|
6992
|
+
<div class="load-script-item-name">${escapeHtml(s.name)}</div>
|
|
6993
|
+
<div class="load-script-item-meta">${escapeHtml(s.date)}</div>
|
|
6994
|
+
<div class="load-script-item-actions">
|
|
6995
|
+
<button class="load-script-load-btn" onclick="loadLoadScript(${JSON.stringify(s.name)})">▶ Загрузить</button>
|
|
6996
|
+
<button class="load-script-del-btn" onclick="loadDeleteScript(${JSON.stringify(s.name)})">✕</button>
|
|
6997
|
+
</div>
|
|
6998
|
+
</div>`).join('')}
|
|
6999
|
+
</div>`
|
|
7000
|
+
}
|
|
6923
7001
|
</div>
|
|
6924
7002
|
|
|
6925
7003
|
${(isRunning || isDone || hasErr) ? `<div class="load-section">
|
|
@@ -7061,6 +7139,139 @@ ${logs.slice(-50).join('\n')}
|
|
|
7061
7139
|
} catch (e) { alert('Ошибка: ' + e.message); }
|
|
7062
7140
|
}
|
|
7063
7141
|
|
|
7142
|
+
async function loadAiGenerateScript() {
|
|
7143
|
+
const promptEl = document.getElementById('loadAiPrompt');
|
|
7144
|
+
const userPrompt = promptEl ? promptEl.value.trim() : loadAiPromptDraft.trim();
|
|
7145
|
+
if (!userPrompt) { alert('Опиши сценарий нагрузочного теста в поле выше'); return; }
|
|
7146
|
+
|
|
7147
|
+
const baseUrl = document.getElementById('loadBaseUrl')?.value || 'http://localhost:3000';
|
|
7148
|
+
const vus = document.getElementById('loadVus')?.value || '10';
|
|
7149
|
+
const duration = document.getElementById('loadDuration')?.value || '30s';
|
|
7150
|
+
|
|
7151
|
+
const featureList = (D && D.features || []).map(f => `- ${f.label} (key: ${f.key})`).join('\n');
|
|
7152
|
+
|
|
7153
|
+
const task = `Сгенерируй k6 скрипт нагрузочного тестирования.
|
|
7154
|
+
|
|
7155
|
+
Сценарий от пользователя:
|
|
7156
|
+
${userPrompt}
|
|
7157
|
+
|
|
7158
|
+
Параметры:
|
|
7159
|
+
- Base URL: ${baseUrl}
|
|
7160
|
+
- VUs: ${vus}
|
|
7161
|
+
- Duration: ${duration}
|
|
7162
|
+
|
|
7163
|
+
Доступные фичи проекта:
|
|
7164
|
+
${featureList || '(нет данных)'}
|
|
7165
|
+
|
|
7166
|
+
Требования к скрипту:
|
|
7167
|
+
1. Валидный JavaScript для k6 (import from 'k6/http', 'k6', 'k6/data' и т.д.)
|
|
7168
|
+
2. export const options = { vus, duration, thresholds }
|
|
7169
|
+
3. export default function() { ... } с проверками check()
|
|
7170
|
+
4. Если нужна загрузка файла — использовать http.file() и open()
|
|
7171
|
+
5. Если нужна авторизация — добавить заголовок Authorization
|
|
7172
|
+
6. Добавить sleep() между запросами
|
|
7173
|
+
7. ТОЛЬКО код скрипта, никаких пояснений снаружи
|
|
7174
|
+
|
|
7175
|
+
Сохрани скрипт СТРОГО в файл: .viberadar/load-script-generated.js
|
|
7176
|
+
Это важно — без этого файла кнопка "Загрузить скрипт" не сработает.`;
|
|
7177
|
+
|
|
7178
|
+
loadAiGenerating = true;
|
|
7179
|
+
loadAiPromptDraft = userPrompt;
|
|
7180
|
+
const statusEl = document.getElementById('loadAiStatus');
|
|
7181
|
+
const btnEl = document.getElementById('loadAiGenBtn');
|
|
7182
|
+
if (statusEl) statusEl.textContent = '⏳ Агент генерирует скрипт… следи в терминале ↓';
|
|
7183
|
+
if (btnEl) btnEl.disabled = true;
|
|
7184
|
+
|
|
7185
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
7186
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
7187
|
+
|
|
7188
|
+
try {
|
|
7189
|
+
const r = await fetch('/api/run-agent', {
|
|
7190
|
+
method: 'POST',
|
|
7191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7192
|
+
body: JSON.stringify({ task: 'custom-prompt', prompt: task }),
|
|
7193
|
+
});
|
|
7194
|
+
if (!r.ok) {
|
|
7195
|
+
const d = await r.json().catch(() => ({}));
|
|
7196
|
+
loadAiGenerating = false;
|
|
7197
|
+
if (statusEl) statusEl.textContent = '❌ Ошибка запуска агента: ' + (d.error || r.status);
|
|
7198
|
+
if (btnEl) btnEl.disabled = false;
|
|
7199
|
+
}
|
|
7200
|
+
} catch (e) {
|
|
7201
|
+
loadAiGenerating = false;
|
|
7202
|
+
if (statusEl) statusEl.textContent = '❌ Ошибка: ' + e.message;
|
|
7203
|
+
if (btnEl) btnEl.disabled = false;
|
|
7204
|
+
}
|
|
7205
|
+
}
|
|
7206
|
+
|
|
7207
|
+
async function loadFetchAiScript() {
|
|
7208
|
+
try {
|
|
7209
|
+
const r = await fetch('/api/load/ai-script');
|
|
7210
|
+
if (!r.ok) return false;
|
|
7211
|
+
const d = await r.json();
|
|
7212
|
+
if (d.script) {
|
|
7213
|
+
loadScriptDraft = d.script;
|
|
7214
|
+
const ta = document.getElementById('loadScriptEditor');
|
|
7215
|
+
if (ta) { ta.value = d.script; ta.style.borderColor = '#8b5cf6'; setTimeout(() => { ta.style.borderColor = ''; }, 2000); }
|
|
7216
|
+
const statusEl = document.getElementById('loadAiStatus');
|
|
7217
|
+
if (statusEl) statusEl.textContent = '✅ Скрипт загружен в редактор';
|
|
7218
|
+
return true;
|
|
7219
|
+
}
|
|
7220
|
+
} catch {}
|
|
7221
|
+
return false;
|
|
7222
|
+
}
|
|
7223
|
+
|
|
7224
|
+
async function loadRefreshScripts() {
|
|
7225
|
+
try {
|
|
7226
|
+
const r = await fetch('/api/load/scripts');
|
|
7227
|
+
if (!r.ok) return;
|
|
7228
|
+
loadSavedScripts = await r.json();
|
|
7229
|
+
renderContent();
|
|
7230
|
+
} catch {}
|
|
7231
|
+
}
|
|
7232
|
+
|
|
7233
|
+
async function loadSaveScript() {
|
|
7234
|
+
const nameEl = document.getElementById('loadScriptName');
|
|
7235
|
+
const name = (nameEl ? nameEl.value : loadScriptNameDraft).trim();
|
|
7236
|
+
if (!name) { alert('Введи название скрипта'); nameEl && nameEl.focus(); return; }
|
|
7237
|
+
const taEl = document.getElementById('loadScriptEditor');
|
|
7238
|
+
const script = taEl ? taEl.value : loadScriptDraft;
|
|
7239
|
+
if (!script.trim()) { alert('Скрипт пустой'); return; }
|
|
7240
|
+
loadScriptNameDraft = name;
|
|
7241
|
+
try {
|
|
7242
|
+
const r = await fetch('/api/load/scripts', {
|
|
7243
|
+
method: 'POST',
|
|
7244
|
+
headers: { 'Content-Type': 'application/json' },
|
|
7245
|
+
body: JSON.stringify({ name, script }),
|
|
7246
|
+
});
|
|
7247
|
+
if (!r.ok) { const d = await r.json().catch(() => ({})); alert('Ошибка: ' + (d.error || r.status)); return; }
|
|
7248
|
+
await loadRefreshScripts();
|
|
7249
|
+
// flash save button
|
|
7250
|
+
const btn = document.querySelector('.load-save-row .load-btn');
|
|
7251
|
+
if (btn) { const orig = btn.textContent; btn.textContent = '✅ Сохранено'; btn.style.color = 'var(--green)'; setTimeout(() => { btn.textContent = orig; btn.style.color = ''; }, 1500); }
|
|
7252
|
+
} catch (e) { alert('Ошибка: ' + e.message); }
|
|
7253
|
+
}
|
|
7254
|
+
|
|
7255
|
+
async function loadLoadScript(name) {
|
|
7256
|
+
const s = loadSavedScripts.find(x => x.name === name);
|
|
7257
|
+
if (!s) return;
|
|
7258
|
+
loadScriptDraft = s.script;
|
|
7259
|
+
loadScriptNameDraft = s.name;
|
|
7260
|
+
const ta = document.getElementById('loadScriptEditor');
|
|
7261
|
+
if (ta) { ta.value = s.script; ta.style.borderColor = 'var(--blue)'; setTimeout(() => { ta.style.borderColor = ''; }, 1500); }
|
|
7262
|
+
const nameEl = document.getElementById('loadScriptName');
|
|
7263
|
+
if (nameEl) nameEl.value = s.name;
|
|
7264
|
+
}
|
|
7265
|
+
|
|
7266
|
+
async function loadDeleteScript(name) {
|
|
7267
|
+
if (!confirm(`Удалить скрипт «${name}»?`)) return;
|
|
7268
|
+
try {
|
|
7269
|
+
const r = await fetch('/api/load/scripts/' + encodeURIComponent(name), { method: 'DELETE' });
|
|
7270
|
+
if (!r.ok) { const d = await r.json().catch(() => ({})); alert('Ошибка: ' + (d.error || r.status)); return; }
|
|
7271
|
+
await loadRefreshScripts();
|
|
7272
|
+
} catch (e) { alert('Ошибка: ' + e.message); }
|
|
7273
|
+
}
|
|
7274
|
+
|
|
7064
7275
|
function connectSSE() {
|
|
7065
7276
|
const es = new EventSource('/api/events');
|
|
7066
7277
|
|
|
@@ -7197,6 +7408,18 @@ function connectSSE() {
|
|
|
7197
7408
|
setDocActualizeStatus(null, null);
|
|
7198
7409
|
docActualizeRunningKey = null;
|
|
7199
7410
|
}
|
|
7411
|
+
// Auto-load AI-generated k6 script if we were waiting for one
|
|
7412
|
+
if (loadAiGenerating) {
|
|
7413
|
+
loadAiGenerating = false;
|
|
7414
|
+
loadFetchAiScript().then(ok => {
|
|
7415
|
+
if (!ok) {
|
|
7416
|
+
const statusEl = document.getElementById('loadAiStatus');
|
|
7417
|
+
if (statusEl) statusEl.textContent = '⚠️ Агент завершил, но файл не найден — возможно скрипт в терминале, скопируй вручную';
|
|
7418
|
+
}
|
|
7419
|
+
const btnEl = document.getElementById('loadAiGenBtn');
|
|
7420
|
+
if (btnEl) btnEl.disabled = false;
|
|
7421
|
+
});
|
|
7422
|
+
}
|
|
7200
7423
|
renderContent();
|
|
7201
7424
|
});
|
|
7202
7425
|
|