viberadar 0.3.174 → 0.3.176

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.
@@ -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>
@@ -1672,6 +1697,8 @@
1672
1697
  let D = null;
1673
1698
  let contextMode = 'qa';
1674
1699
  let view = 'features';
1700
+ let probeData = null; // { status, lastRun, intervalSec, nextRunAt, checks, configFound }
1701
+ let probeRunning = false;
1675
1702
  let searchQuery = '';
1676
1703
  let activeTypes = new Set();
1677
1704
  let activePanelKey = null;
@@ -1780,13 +1807,14 @@ function switchMode(nextMode) {
1780
1807
  saveModeState(contextMode);
1781
1808
  contextMode = nextMode;
1782
1809
  restoreModeState(contextMode);
1783
- if (contextMode === 'observability' || contextMode === 'docs' || contextMode === 'services' || contextMode === 'load') {
1810
+ if (contextMode === 'observability' || contextMode === 'docs' || contextMode === 'services' || contextMode === 'load' || contextMode === 'probe') {
1784
1811
  view = 'features';
1785
1812
  drillFeatureKey = null;
1786
1813
  drillTestType = null;
1787
1814
  activePanelKey = null;
1788
1815
  clearFeatureHash();
1789
1816
  }
1817
+ if (contextMode === 'probe') { loadProbeData(); }
1790
1818
  if (contextMode === 'load' && loadK6Available === null) { checkK6(); }
1791
1819
  if (contextMode === 'load') {
1792
1820
  loadRefreshScripts();
@@ -3155,6 +3183,7 @@ function renderModeSwitch() {
3155
3183
  { key: 'scenarios', label: 'Сценарии', hint: 'Пользовательские сценарии, user journeys' },
3156
3184
  { key: 'services', label: 'Карта сервисов', hint: 'Зависимости, пайплайны, мониторинг' },
3157
3185
  { key: 'load', label: 'Нагрузка', hint: 'k6: метрики, сценарии, AI-анализ' },
3186
+ { key: 'probe', label: 'Probe', hint: 'Synthetic monitoring, E2E на стенде' },
3158
3187
  ];
3159
3188
  root.innerHTML = modes.map(m => `
3160
3189
  <button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
@@ -3173,6 +3202,16 @@ function renderSidebar() {
3173
3202
  const tabs = document.getElementById('viewTabs');
3174
3203
  const extra = document.getElementById('sidebarExtra');
3175
3204
 
3205
+ if (contextMode === 'probe') {
3206
+ tabs.style.display = 'none';
3207
+ extra.innerHTML = `
3208
+ <div class="sidebar-label">Probe</div>
3209
+ <div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
3210
+ Запуск E2E-проверок на стенде. Домен задаётся в Настройках.
3211
+ </div>`;
3212
+ return;
3213
+ }
3214
+
3176
3215
  if (contextMode === 'observability') {
3177
3216
  tabs.style.display = 'none';
3178
3217
  extra.innerHTML = `
@@ -3318,6 +3357,11 @@ function renderContent() {
3318
3357
  return;
3319
3358
  }
3320
3359
 
3360
+ if (contextMode === 'probe') {
3361
+ renderProbePanel(c);
3362
+ return;
3363
+ }
3364
+
3321
3365
  if (view === 'features') {
3322
3366
  if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
3323
3367
  else if (drillFeatureKey) renderFeatureDetail(c);
@@ -3329,6 +3373,161 @@ function renderContent() {
3329
3373
  }
3330
3374
  }
3331
3375
 
3376
+ // ─── Probe rendering ────────────────────────────────────────────────────────
3377
+
3378
+ async function loadProbeData() {
3379
+ try {
3380
+ const res = await fetch('/api/probe/status');
3381
+ probeData = await res.json();
3382
+ } catch {}
3383
+ }
3384
+
3385
+ function renderProbePanel(c) {
3386
+ const d = probeData;
3387
+ const statusColor = !d ? 'var(--muted)' : d.status === 'running' ? 'var(--yellow)' : d.status === 'scheduled' ? 'var(--green)' : 'var(--muted)';
3388
+ const statusLabel = !d ? '—' : d.status === 'running' ? '● running' : d.status === 'scheduled' ? '● scheduled' : '○ idle';
3389
+
3390
+ const lastRunHtml = d && d.lastRun ? (() => {
3391
+ const r = d.lastRun;
3392
+ const allPassed = r.failed === 0;
3393
+ const rows = r.results.map(res => {
3394
+ const ok = res.status === 'passed';
3395
+ return `<div class="probe-check-row">
3396
+ <span class="probe-check-icon">${ok ? '✅' : '❌'}</span>
3397
+ <span class="probe-check-name">${escapeHtml(res.check)}</span>
3398
+ <span class="probe-check-dur" style="color:var(--muted)">${res.durationMs}ms</span>
3399
+ ${!ok && res.error ? `<div class="probe-check-error">${escapeHtml(res.error)}</div>` : ''}
3400
+ ${!ok && res.screenshotPath ? `<div class="probe-check-error" style="color:var(--muted)">📸 ${escapeHtml(res.screenshotPath)}</div>` : ''}
3401
+ </div>`;
3402
+ }).join('');
3403
+ return `
3404
+ <div class="probe-section">
3405
+ <div class="probe-section-title">Последний прогон <span style="color:var(--muted);font-weight:400;font-size:11px">${r.timestamp ? new Date(r.timestamp).toLocaleTimeString() : ''}</span>
3406
+ <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>
3407
+ </div>
3408
+ <div class="probe-checks">${rows}</div>
3409
+ </div>`;
3410
+ })() : `<div style="color:var(--muted);font-size:13px;padding:16px 0">Ещё не запускался</div>`;
3411
+
3412
+ const intervalOptions = [60, 300, 600, 1800, 3600].map(s => {
3413
+ const label = s < 3600 ? `каждые ${s / 60} мин` : `каждый час`;
3414
+ const sel = d && d.intervalSec === s ? ' selected' : '';
3415
+ return `<option value="${s}"${sel}>${label}</option>`;
3416
+ }).join('');
3417
+
3418
+ const isScheduled = d && d.status === 'scheduled';
3419
+ const isRunning = d && d.status === 'running';
3420
+
3421
+ c.innerHTML = `
3422
+ <div class="probe-container">
3423
+ <div class="probe-header">
3424
+ <div>
3425
+ <div class="probe-title">🔭 Probe monitoring</div>
3426
+ ${d && d.effectiveTarget ? `<div style="color:var(--muted);font-size:12px">${escapeHtml(d.effectiveTarget)} · ${escapeHtml((d.checks || []).length)} checks · <span style="color:${statusColor}">${statusLabel}</span></div>`
3427
+ : `<div style="color:var(--yellow);font-size:12px">⚠️ Задайте домен стенда в Настройках</div>`}
3428
+ </div>
3429
+ <button class="probe-btn" onclick="openProbeSettingsModal()">⚙️ Настройки</button>
3430
+ </div>
3431
+
3432
+ <div class="probe-section">
3433
+ <div class="probe-controls">
3434
+ <button class="probe-btn probe-btn-run" onclick="probeRunNow()" ${isRunning ? 'disabled' : ''}>
3435
+ ${isRunning ? '⏳ Выполняется…' : '▶ Run Now'}
3436
+ </button>
3437
+ <div class="probe-schedule-row">
3438
+ <select id="probeIntervalSelect" class="probe-select">
3439
+ ${intervalOptions}
3440
+ </select>
3441
+ ${isScheduled
3442
+ ? `<button class="probe-btn probe-btn-stop" onclick="probeScheduleStop()">⏹ Стоп</button>`
3443
+ : `<button class="probe-btn probe-btn-start" onclick="probeScheduleStart()">🕐 По расписанию</button>`
3444
+ }
3445
+ </div>
3446
+ </div>
3447
+ ${isScheduled && d.nextRunAt ? `<div style="font-size:11px;color:var(--muted);margin-top:6px">Следующий запуск: ${new Date(d.nextRunAt).toLocaleTimeString()}</div>` : ''}
3448
+ </div>
3449
+
3450
+ ${lastRunHtml}
3451
+ </div>`;
3452
+ }
3453
+
3454
+ async function probeRunNow() {
3455
+ probeRunning = true;
3456
+ renderContent();
3457
+ try { await fetch('/api/probe/run', { method: 'POST' }); } catch {}
3458
+ }
3459
+
3460
+ async function probeScheduleStart() {
3461
+ const sel = document.getElementById('probeIntervalSelect');
3462
+ const intervalSec = sel ? parseInt(sel.value) : 600;
3463
+ await fetch('/api/probe/schedule/start', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ intervalSec }) });
3464
+ await loadProbeData();
3465
+ renderContent();
3466
+ }
3467
+
3468
+ async function probeScheduleStop() {
3469
+ await fetch('/api/probe/schedule/stop', { method: 'POST' });
3470
+ await loadProbeData();
3471
+ renderContent();
3472
+ }
3473
+
3474
+ function openProbeSettingsModal() {
3475
+ fetch('/api/probe/settings').then(r => r.json()).then(saved => {
3476
+ const tg = saved.telegram || {};
3477
+ const inp = s => `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"`;
3478
+ const lbl = t => `<label style="font-size:12px;color:var(--muted);display:block;margin:12px 0 4px">${t}</label>`;
3479
+ let overlay = document.getElementById('probeSettingsOverlay');
3480
+ if (overlay) overlay.remove();
3481
+ overlay = document.createElement('div');
3482
+ overlay.id = 'probeSettingsOverlay';
3483
+ 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';
3484
+ overlay.innerHTML = `
3485
+ <div style="background:var(--bg-card);border:1px solid var(--border);border-radius:12px;padding:24px 28px;width:440px;max-width:90vw">
3486
+ <h3 style="margin:0 0 4px;font-size:16px;color:var(--text)">⚙️ Настройки Probe</h3>
3487
+ <div style="font-size:12px;color:var(--muted);margin-bottom:16px">Настройки сохраняются локально, не попадают в git</div>
3488
+
3489
+ <div style="border-bottom:1px solid var(--border);padding-bottom:16px;margin-bottom:4px">
3490
+ ${lbl('Домен стенда')}
3491
+ <input id="probeTarget" type="text" placeholder="https://staging.myapp.com" value="${escapeHtml(saved.target || '')}" ${inp()}>
3492
+ <div style="font-size:11px;color:var(--muted);margin-top:4px">Все проверки будут запускаться на этом домене. Переопределяет target из probe.config.yml</div>
3493
+ </div>
3494
+
3495
+ <div style="margin-top:16px">
3496
+ <div style="font-size:12px;font-weight:600;color:var(--text);margin-bottom:8px">Telegram уведомления</div>
3497
+ ${lbl('Bot Token')}
3498
+ <input id="probeBotToken" type="password" placeholder="1234567890:AAF..." value="${escapeHtml(tg.botToken || '')}" ${inp()}>
3499
+ ${lbl('Chat ID')}
3500
+ <input id="probeChatId" type="text" placeholder="-1001234567890" value="${escapeHtml(tg.chatId || '')}" ${inp()}>
3501
+ <div style="font-size:11px;color:var(--muted);margin-top:4px">Токен у @BotFather · Chat ID через @userinfobot</div>
3502
+ </div>
3503
+
3504
+ <div id="probeSettingsErr" style="display:none;margin-top:12px;padding:8px 12px;border-radius:6px;background:rgba(248,81,73,0.1);color:var(--red);font-size:12px"></div>
3505
+ <div style="display:flex;gap:10px;justify-content:flex-end;margin-top:20px">
3506
+ <button onclick="document.getElementById('probeSettingsOverlay').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>
3507
+ <button id="probeSettingsSave" style="padding:6px 16px;border-radius:6px;border:1px solid var(--blue);background:var(--blue);color:#fff;cursor:pointer;font-size:13px">Сохранить</button>
3508
+ </div>
3509
+ </div>`;
3510
+ document.body.appendChild(overlay);
3511
+ overlay.addEventListener('click', e => { if (e.target === overlay) overlay.remove(); });
3512
+ document.getElementById('probeSettingsSave').addEventListener('click', async () => {
3513
+ const target = document.getElementById('probeTarget').value.trim();
3514
+ const botToken = document.getElementById('probeBotToken').value.trim();
3515
+ const chatId = document.getElementById('probeChatId').value.trim();
3516
+ const err = document.getElementById('probeSettingsErr');
3517
+ const btn = document.getElementById('probeSettingsSave');
3518
+ btn.disabled = true; btn.textContent = 'Сохраняю…';
3519
+ try {
3520
+ const res = await fetch('/api/probe/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ target, botToken, chatId }) });
3521
+ if (!res.ok) { const d = await res.json(); throw new Error(d.error || 'Ошибка сохранения'); }
3522
+ // Refresh probe data to show new target
3523
+ await loadProbeData();
3524
+ renderContent();
3525
+ overlay.remove();
3526
+ } catch (e) { err.style.display = 'block'; err.textContent = e.message; btn.disabled = false; btn.textContent = 'Сохранить'; }
3527
+ });
3528
+ });
3529
+ }
3530
+
3332
3531
  // ─── Service Map rendering ──────────────────────────────────────────────────
3333
3532
 
3334
3533
  const SVC_CATEGORY_COLORS = {
@@ -6735,6 +6934,7 @@ document.querySelectorAll('.view-tab').forEach(tab => {
6735
6934
  document.getElementById('searchInput').value = '';
6736
6935
  document.getElementById('panel').classList.remove('open');
6737
6936
  if (view !== 'features') clearFeatureHash();
6937
+ if (view === 'probe') { loadProbeData().then(() => { renderSidebar(); renderContent(); }); return; }
6738
6938
  renderSidebar();
6739
6939
  renderContent();
6740
6940
  };
@@ -7791,6 +7991,31 @@ function connectSSE() {
7791
7991
  if (contextMode === 'load') { renderSidebar(); renderContent(); }
7792
7992
  });
7793
7993
 
7994
+ es.addEventListener('probe-run-started', () => {
7995
+ if (!probeData) probeData = {};
7996
+ probeData.status = 'running';
7997
+ probeRunning = true;
7998
+ if (view === 'probe') renderContent();
7999
+ });
8000
+
8001
+ es.addEventListener('probe-run-done', (e) => {
8002
+ const payload = JSON.parse(e.data || '{}');
8003
+ if (!probeData) probeData = {};
8004
+ if (payload.results) probeData.lastRun = payload;
8005
+ probeData.status = probeData.intervalSec ? 'scheduled' : 'idle';
8006
+ probeRunning = false;
8007
+ if (view === 'probe') renderContent();
8008
+ });
8009
+
8010
+ es.addEventListener('probe-scheduled', (e) => {
8011
+ const payload = JSON.parse(e.data || '{}');
8012
+ if (!probeData) probeData = {};
8013
+ probeData.status = payload.status;
8014
+ probeData.intervalSec = payload.intervalSec;
8015
+ probeData.nextRunAt = payload.nextRunAt;
8016
+ if (view === 'probe') renderContent();
8017
+ });
8018
+
7794
8019
  es.onerror = () => {
7795
8020
  setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
7796
8021
  es.close();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.174",
3
+ "version": "0.3.176",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {