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.
@@ -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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.174",
3
+ "version": "0.3.175",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {