viberadar 0.3.174 → 0.3.175
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/cli.js +5 -2
- package/dist/cli.js.map +1 -1
- package/dist/probe/config.d.ts +2 -1
- package/dist/probe/config.d.ts.map +1 -1
- package/dist/probe/config.js +22 -12
- package/dist/probe/config.js.map +1 -1
- package/dist/probe/index.js +1 -1
- package/dist/probe/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +144 -0
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +199 -0
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -1560,6 +1560,31 @@
|
|
|
1560
1560
|
.load-no-k6 h3 { color: var(--text); margin-bottom: 8px; }
|
|
1561
1561
|
.load-no-k6 code { background: var(--bg); border: 1px solid var(--border); padding: 3px 8px; border-radius: 4px; font-size: 13px; color: var(--blue); }
|
|
1562
1562
|
|
|
1563
|
+
/* Probe panel */
|
|
1564
|
+
.probe-container { padding: 20px 24px; max-width: 720px; }
|
|
1565
|
+
.probe-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 20px; gap: 12px; }
|
|
1566
|
+
.probe-title { font-size: 16px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
|
|
1567
|
+
.probe-section { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 14px; }
|
|
1568
|
+
.probe-section-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
|
|
1569
|
+
.probe-controls { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
|
1570
|
+
.probe-schedule-row { display: flex; align-items: center; gap: 8px; }
|
|
1571
|
+
.probe-select { padding: 6px 10px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 12px; cursor: pointer; }
|
|
1572
|
+
.probe-btn { padding: 6px 14px; border-radius: 6px; border: 1px solid var(--border); background: var(--bg); color: var(--text); font-size: 12px; cursor: pointer; transition: background 0.15s; white-space: nowrap; }
|
|
1573
|
+
.probe-btn:hover { background: var(--bg-hover); }
|
|
1574
|
+
.probe-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
1575
|
+
.probe-btn-run { border-color: var(--green); color: var(--green); }
|
|
1576
|
+
.probe-btn-run:hover { background: rgba(63,185,80,0.1); }
|
|
1577
|
+
.probe-btn-start { border-color: var(--blue); color: var(--blue); }
|
|
1578
|
+
.probe-btn-start:hover { background: rgba(88,166,255,0.1); }
|
|
1579
|
+
.probe-btn-stop { border-color: var(--red); color: var(--red); }
|
|
1580
|
+
.probe-btn-stop:hover { background: rgba(248,81,73,0.1); }
|
|
1581
|
+
.probe-checks { display: flex; flex-direction: column; gap: 8px; }
|
|
1582
|
+
.probe-check-row { display: grid; grid-template-columns: 20px 1fr auto; align-items: baseline; gap: 6px; font-size: 13px; }
|
|
1583
|
+
.probe-check-icon { font-size: 14px; }
|
|
1584
|
+
.probe-check-name { color: var(--text); }
|
|
1585
|
+
.probe-check-dur { font-size: 11px; }
|
|
1586
|
+
.probe-check-error { grid-column: 2 / -1; font-size: 11px; color: var(--red); margin-top: 2px; word-break: break-word; }
|
|
1587
|
+
|
|
1563
1588
|
</style>
|
|
1564
1589
|
</head>
|
|
1565
1590
|
<body>
|
|
@@ -1616,6 +1641,7 @@
|
|
|
1616
1641
|
<div class="view-tab" data-view="features">Features</div>
|
|
1617
1642
|
<div class="view-tab" data-view="files">Files</div>
|
|
1618
1643
|
<div class="view-tab" data-view="tests">Tests</div>
|
|
1644
|
+
<div class="view-tab" data-view="probe">Probe</div>
|
|
1619
1645
|
</div>
|
|
1620
1646
|
<input class="search-input" id="searchInput" type="text" placeholder="Search…" />
|
|
1621
1647
|
<div id="sidebarExtra"></div>
|
|
@@ -1672,6 +1698,8 @@
|
|
|
1672
1698
|
let D = null;
|
|
1673
1699
|
let contextMode = 'qa';
|
|
1674
1700
|
let view = 'features';
|
|
1701
|
+
let probeData = null; // { status, lastRun, intervalSec, nextRunAt, checks, configFound }
|
|
1702
|
+
let probeRunning = false;
|
|
1675
1703
|
let searchQuery = '';
|
|
1676
1704
|
let activeTypes = new Set();
|
|
1677
1705
|
let activePanelKey = null;
|
|
@@ -3318,6 +3346,11 @@ function renderContent() {
|
|
|
3318
3346
|
return;
|
|
3319
3347
|
}
|
|
3320
3348
|
|
|
3349
|
+
if (view === 'probe') {
|
|
3350
|
+
renderProbePanel(c);
|
|
3351
|
+
return;
|
|
3352
|
+
}
|
|
3353
|
+
|
|
3321
3354
|
if (view === 'features') {
|
|
3322
3355
|
if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
|
|
3323
3356
|
else if (drillFeatureKey) renderFeatureDetail(c);
|
|
@@ -3329,6 +3362,146 @@ function renderContent() {
|
|
|
3329
3362
|
}
|
|
3330
3363
|
}
|
|
3331
3364
|
|
|
3365
|
+
// ─── Probe rendering ────────────────────────────────────────────────────────
|
|
3366
|
+
|
|
3367
|
+
async function loadProbeData() {
|
|
3368
|
+
try {
|
|
3369
|
+
const res = await fetch('/api/probe/status');
|
|
3370
|
+
probeData = await res.json();
|
|
3371
|
+
} catch {}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
function renderProbePanel(c) {
|
|
3375
|
+
const d = probeData;
|
|
3376
|
+
const statusColor = !d ? 'var(--muted)' : d.status === 'running' ? 'var(--yellow)' : d.status === 'scheduled' ? 'var(--green)' : 'var(--muted)';
|
|
3377
|
+
const statusLabel = !d ? '—' : d.status === 'running' ? '● running' : d.status === 'scheduled' ? '● scheduled' : '○ idle';
|
|
3378
|
+
|
|
3379
|
+
const lastRunHtml = d && d.lastRun ? (() => {
|
|
3380
|
+
const r = d.lastRun;
|
|
3381
|
+
const allPassed = r.failed === 0;
|
|
3382
|
+
const rows = r.results.map(res => {
|
|
3383
|
+
const ok = res.status === 'passed';
|
|
3384
|
+
return `<div class="probe-check-row">
|
|
3385
|
+
<span class="probe-check-icon">${ok ? '✅' : '❌'}</span>
|
|
3386
|
+
<span class="probe-check-name">${escapeHtml(res.check)}</span>
|
|
3387
|
+
<span class="probe-check-dur" style="color:var(--muted)">${res.durationMs}ms</span>
|
|
3388
|
+
${!ok && res.error ? `<div class="probe-check-error">${escapeHtml(res.error)}</div>` : ''}
|
|
3389
|
+
${!ok && res.screenshotPath ? `<div class="probe-check-error" style="color:var(--muted)">📸 ${escapeHtml(res.screenshotPath)}</div>` : ''}
|
|
3390
|
+
</div>`;
|
|
3391
|
+
}).join('');
|
|
3392
|
+
return `
|
|
3393
|
+
<div class="probe-section">
|
|
3394
|
+
<div class="probe-section-title">Последний прогон <span style="color:var(--muted);font-weight:400;font-size:11px">${r.timestamp ? new Date(r.timestamp).toLocaleTimeString() : ''}</span>
|
|
3395
|
+
<span style="margin-left:8px;font-size:12px">${allPassed ? `<span style="color:var(--green)">✅ ${r.passed}/${r.results.length}</span>` : `<span style="color:var(--red)">❌ ${r.failed} failed</span>`}</span>
|
|
3396
|
+
</div>
|
|
3397
|
+
<div class="probe-checks">${rows}</div>
|
|
3398
|
+
</div>`;
|
|
3399
|
+
})() : `<div style="color:var(--muted);font-size:13px;padding:16px 0">Ещё не запускался</div>`;
|
|
3400
|
+
|
|
3401
|
+
const intervalOptions = [60, 300, 600, 1800, 3600].map(s => {
|
|
3402
|
+
const label = s < 3600 ? `каждые ${s / 60} мин` : `каждый час`;
|
|
3403
|
+
const sel = d && d.intervalSec === s ? ' selected' : '';
|
|
3404
|
+
return `<option value="${s}"${sel}>${label}</option>`;
|
|
3405
|
+
}).join('');
|
|
3406
|
+
|
|
3407
|
+
const isScheduled = d && d.status === 'scheduled';
|
|
3408
|
+
const isRunning = d && d.status === 'running';
|
|
3409
|
+
|
|
3410
|
+
c.innerHTML = `
|
|
3411
|
+
<div class="probe-container">
|
|
3412
|
+
<div class="probe-header">
|
|
3413
|
+
<div>
|
|
3414
|
+
<div class="probe-title">🔭 Probe monitoring</div>
|
|
3415
|
+
${d && d.configFound ? `<div style="color:var(--muted);font-size:12px">${escapeHtml((d.checks || []).length)} checks · <span style="color:${statusColor}">${statusLabel}</span></div>`
|
|
3416
|
+
: `<div style="color:var(--yellow);font-size:12px">⚠️ probe.config.yml не найден в рабочей директории</div>`}
|
|
3417
|
+
</div>
|
|
3418
|
+
<button class="probe-btn" onclick="openProbeNotifyModal()">⚙️ Уведомления</button>
|
|
3419
|
+
</div>
|
|
3420
|
+
|
|
3421
|
+
<div class="probe-section">
|
|
3422
|
+
<div class="probe-controls">
|
|
3423
|
+
<button class="probe-btn probe-btn-run" onclick="probeRunNow()" ${isRunning ? 'disabled' : ''}>
|
|
3424
|
+
${isRunning ? '⏳ Выполняется…' : '▶ Run Now'}
|
|
3425
|
+
</button>
|
|
3426
|
+
<div class="probe-schedule-row">
|
|
3427
|
+
<select id="probeIntervalSelect" class="probe-select">
|
|
3428
|
+
${intervalOptions}
|
|
3429
|
+
</select>
|
|
3430
|
+
${isScheduled
|
|
3431
|
+
? `<button class="probe-btn probe-btn-stop" onclick="probeScheduleStop()">⏹ Стоп</button>`
|
|
3432
|
+
: `<button class="probe-btn probe-btn-start" onclick="probeScheduleStart()">🕐 По расписанию</button>`
|
|
3433
|
+
}
|
|
3434
|
+
</div>
|
|
3435
|
+
</div>
|
|
3436
|
+
${isScheduled && d.nextRunAt ? `<div style="font-size:11px;color:var(--muted);margin-top:6px">Следующий запуск: ${new Date(d.nextRunAt).toLocaleTimeString()}</div>` : ''}
|
|
3437
|
+
</div>
|
|
3438
|
+
|
|
3439
|
+
${lastRunHtml}
|
|
3440
|
+
</div>`;
|
|
3441
|
+
}
|
|
3442
|
+
|
|
3443
|
+
async function probeRunNow() {
|
|
3444
|
+
probeRunning = true;
|
|
3445
|
+
renderContent();
|
|
3446
|
+
try { await fetch('/api/probe/run', { method: 'POST' }); } catch {}
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
async function probeScheduleStart() {
|
|
3450
|
+
const sel = document.getElementById('probeIntervalSelect');
|
|
3451
|
+
const intervalSec = sel ? parseInt(sel.value) : 600;
|
|
3452
|
+
await fetch('/api/probe/schedule/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ intervalSec }) });
|
|
3453
|
+
await loadProbeData();
|
|
3454
|
+
renderContent();
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
async function probeScheduleStop() {
|
|
3458
|
+
await fetch('/api/probe/schedule/stop', { method: 'POST' });
|
|
3459
|
+
await loadProbeData();
|
|
3460
|
+
renderContent();
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
function openProbeNotifyModal() {
|
|
3464
|
+
fetch('/api/probe/notify-config').then(r => r.json()).then(saved => {
|
|
3465
|
+
const tg = saved.telegram || {};
|
|
3466
|
+
let overlay = document.getElementById('probeNotifyOverlay');
|
|
3467
|
+
if (overlay) overlay.remove();
|
|
3468
|
+
overlay = document.createElement('div');
|
|
3469
|
+
overlay.id = 'probeNotifyOverlay';
|
|
3470
|
+
overlay.style.cssText = 'position:fixed;inset:0;background:rgba(0,0,0,0.6);display:flex;align-items:center;justify-content:center;z-index:9999';
|
|
3471
|
+
overlay.innerHTML = `
|
|
3472
|
+
<div style="background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:24px 28px;width:400px;max-width:90vw">
|
|
3473
|
+
<h3 style="margin:0 0 16px;font-size:16px;color:var(--text)">⚙️ Telegram уведомления</h3>
|
|
3474
|
+
<label style="font-size:12px;color:var(--muted);display:block;margin-bottom:4px">Bot Token</label>
|
|
3475
|
+
<input id="probeBotToken" type="password" placeholder="1234567890:AAF..." value="${escapeHtml(tg.botToken || '')}"
|
|
3476
|
+
style="width:100%;box-sizing:border-box;padding:8px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:13px;margin-bottom:12px">
|
|
3477
|
+
<label style="font-size:12px;color:var(--muted);display:block;margin-bottom:4px">Chat ID</label>
|
|
3478
|
+
<input id="probeChatId" type="text" placeholder="-1001234567890" value="${escapeHtml(tg.chatId || '')}"
|
|
3479
|
+
style="width:100%;box-sizing:border-box;padding:8px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);font-size:13px;margin-bottom:4px">
|
|
3480
|
+
<div style="font-size:11px;color:var(--muted);margin-bottom:16px">Получить токен у @BotFather · Chat ID через @userinfobot</div>
|
|
3481
|
+
<div id="probeNotifyErr" style="display:none;margin-bottom:12px;padding:8px 12px;border-radius:6px;background:rgba(248,81,73,0.1);color:var(--red);font-size:12px"></div>
|
|
3482
|
+
<div style="display:flex;gap:10px;justify-content:flex-end">
|
|
3483
|
+
<button onclick="document.getElementById('probeNotifyOverlay').remove()" style="padding:6px 16px;border-radius:6px;border:1px solid var(--border);background:var(--bg);color:var(--text);cursor:pointer;font-size:13px">Отмена</button>
|
|
3484
|
+
<button id="probeNotifySave" style="padding:6px 16px;border-radius:6px;border:1px solid var(--blue);background:var(--blue);color:#fff;cursor:pointer;font-size:13px">Сохранить</button>
|
|
3485
|
+
</div>
|
|
3486
|
+
</div>`;
|
|
3487
|
+
document.body.appendChild(overlay);
|
|
3488
|
+
overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
|
|
3489
|
+
document.getElementById('probeNotifySave').addEventListener('click', async () => {
|
|
3490
|
+
const botToken = document.getElementById('probeBotToken').value.trim();
|
|
3491
|
+
const chatId = document.getElementById('probeChatId').value.trim();
|
|
3492
|
+
const err = document.getElementById('probeNotifyErr');
|
|
3493
|
+
if (!botToken || !chatId) { err.style.display = 'block'; err.textContent = 'Введите Bot Token и Chat ID'; return; }
|
|
3494
|
+
const btn = document.getElementById('probeNotifySave');
|
|
3495
|
+
btn.disabled = true; btn.textContent = 'Сохраняю…';
|
|
3496
|
+
try {
|
|
3497
|
+
const res = await fetch('/api/probe/notify-config', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ botToken, chatId }) });
|
|
3498
|
+
if (!res.ok) { const d = await res.json(); throw new Error(d.error || 'Ошибка сохранения'); }
|
|
3499
|
+
overlay.remove();
|
|
3500
|
+
} catch (e) { err.style.display = 'block'; err.textContent = e.message; btn.disabled = false; btn.textContent = 'Сохранить'; }
|
|
3501
|
+
});
|
|
3502
|
+
});
|
|
3503
|
+
}
|
|
3504
|
+
|
|
3332
3505
|
// ─── Service Map rendering ──────────────────────────────────────────────────
|
|
3333
3506
|
|
|
3334
3507
|
const SVC_CATEGORY_COLORS = {
|
|
@@ -6735,6 +6908,7 @@ document.querySelectorAll('.view-tab').forEach(tab => {
|
|
|
6735
6908
|
document.getElementById('searchInput').value = '';
|
|
6736
6909
|
document.getElementById('panel').classList.remove('open');
|
|
6737
6910
|
if (view !== 'features') clearFeatureHash();
|
|
6911
|
+
if (view === 'probe') { loadProbeData().then(() => { renderSidebar(); renderContent(); }); return; }
|
|
6738
6912
|
renderSidebar();
|
|
6739
6913
|
renderContent();
|
|
6740
6914
|
};
|
|
@@ -7791,6 +7965,31 @@ function connectSSE() {
|
|
|
7791
7965
|
if (contextMode === 'load') { renderSidebar(); renderContent(); }
|
|
7792
7966
|
});
|
|
7793
7967
|
|
|
7968
|
+
es.addEventListener('probe-run-started', () => {
|
|
7969
|
+
if (!probeData) probeData = {};
|
|
7970
|
+
probeData.status = 'running';
|
|
7971
|
+
probeRunning = true;
|
|
7972
|
+
if (view === 'probe') renderContent();
|
|
7973
|
+
});
|
|
7974
|
+
|
|
7975
|
+
es.addEventListener('probe-run-done', (e) => {
|
|
7976
|
+
const payload = JSON.parse(e.data || '{}');
|
|
7977
|
+
if (!probeData) probeData = {};
|
|
7978
|
+
if (payload.results) probeData.lastRun = payload;
|
|
7979
|
+
probeData.status = probeData.intervalSec ? 'scheduled' : 'idle';
|
|
7980
|
+
probeRunning = false;
|
|
7981
|
+
if (view === 'probe') renderContent();
|
|
7982
|
+
});
|
|
7983
|
+
|
|
7984
|
+
es.addEventListener('probe-scheduled', (e) => {
|
|
7985
|
+
const payload = JSON.parse(e.data || '{}');
|
|
7986
|
+
if (!probeData) probeData = {};
|
|
7987
|
+
probeData.status = payload.status;
|
|
7988
|
+
probeData.intervalSec = payload.intervalSec;
|
|
7989
|
+
probeData.nextRunAt = payload.nextRunAt;
|
|
7990
|
+
if (view === 'probe') renderContent();
|
|
7991
|
+
});
|
|
7992
|
+
|
|
7794
7993
|
es.onerror = () => {
|
|
7795
7994
|
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
7796
7995
|
es.close();
|