viberadar 0.3.179 → 0.3.181

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.
@@ -1561,8 +1561,8 @@
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
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; }
1564
+ .probe-container { padding: 20px 24px; max-width: 960px; }
1565
+ .probe-header { display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 16px; gap: 12px; }
1566
1566
  .probe-title { font-size: 16px; font-weight: 700; color: var(--text); margin-bottom: 4px; }
1567
1567
  .probe-section { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; margin-bottom: 14px; }
1568
1568
  .probe-section-title { font-size: 12px; font-weight: 600; color: var(--muted); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 12px; }
@@ -1578,12 +1578,40 @@
1578
1578
  .probe-btn-start:hover { background: rgba(88,166,255,0.1); }
1579
1579
  .probe-btn-stop { border-color: var(--red); color: var(--red); }
1580
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; }
1581
+ /* Probe cards */
1582
+ .probe-cards { display: grid; grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); gap: 12px; margin-top: 16px; }
1583
+ .probe-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; padding: 16px; cursor: pointer; transition: border-color 0.15s, background 0.15s; display: flex; flex-direction: column; gap: 0; }
1584
+ .probe-card:hover { border-color: var(--blue); background: var(--bg-hover); }
1585
+ .probe-card.running { border-color: var(--yellow, #e3b341); }
1586
+ .probe-card.passed { border-color: var(--green); }
1587
+ .probe-card.failed { border-color: var(--red); }
1588
+ .probe-card-top { display: flex; align-items: flex-start; justify-content: space-between; gap: 8px; }
1589
+ .probe-card-name { font-size: 13px; font-weight: 600; color: var(--text); flex: 1; line-height: 1.4; }
1590
+ .probe-card-icon { font-size: 20px; flex-shrink: 0; }
1591
+ @keyframes spin { to { transform: rotate(360deg); } }
1592
+ .probe-spin { display: inline-block; animation: spin 1s linear infinite; }
1593
+ .probe-card-meta { display: flex; gap: 6px; align-items: center; margin-top: 6px; flex-wrap: wrap; }
1594
+ .probe-badge { background: var(--bg); border: 1px solid var(--border); border-radius: 3px; padding: 1px 5px; font-size: 10px; color: var(--muted); }
1595
+ .probe-card-dur { font-size: 11px; color: var(--muted); }
1596
+ .probe-card-err { font-size: 11px; color: var(--red); margin-top: 6px; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.4; }
1597
+ .probe-card-footer { display: flex; align-items: center; justify-content: flex-end; margin-top: 12px; }
1598
+ /* Probe detail */
1599
+ .probe-detail { padding: 20px 24px; max-width: 860px; }
1600
+ .probe-back { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; color: var(--blue); cursor: pointer; margin-bottom: 16px; }
1601
+ .probe-back:hover { text-decoration: underline; }
1602
+ .probe-detail-header { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 20px; }
1603
+ .probe-detail-title { font-size: 18px; font-weight: 700; color: var(--text); }
1604
+ .probe-detail-meta { font-size: 12px; color: var(--muted); margin-top: 4px; display: flex; gap: 10px; flex-wrap: wrap; align-items: center; }
1605
+ .probe-status-badge { display: inline-flex; align-items: center; gap: 4px; font-size: 12px; font-weight: 600; padding: 2px 8px; border-radius: 4px; }
1606
+ .probe-status-passed { background: rgba(63,185,80,0.12); color: var(--green); }
1607
+ .probe-status-failed { background: rgba(248,81,73,0.12); color: var(--red); }
1608
+ .probe-status-running { background: rgba(227,179,65,0.12); color: #e3b341; }
1609
+ .probe-status-idle { background: var(--bg); color: var(--muted); border: 1px solid var(--border); }
1610
+ .probe-steps-list { display: flex; flex-direction: column; gap: 4px; margin-top: 4px; }
1611
+ .probe-step-row { display: flex; align-items: center; gap: 8px; font-size: 12px; padding: 7px 12px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; }
1612
+ .probe-step-type { font-size: 10px; font-weight: 600; color: var(--blue); background: rgba(88,166,255,0.1); padding: 1px 5px; border-radius: 3px; min-width: 80px; text-align: center; }
1613
+ .probe-step-val { color: var(--text); font-family: monospace; }
1614
+ .probe-output-box { font-family: 'SFMono-Regular', Consolas, monospace; font-size: 11px; background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: 12px 14px; white-space: pre-wrap; overflow-x: auto; max-height: 360px; overflow-y: auto; color: var(--text); line-height: 1.5; margin-top: 8px; }
1587
1615
 
1588
1616
  </style>
1589
1617
  </head>
@@ -1697,8 +1725,9 @@
1697
1725
  let D = null;
1698
1726
  let contextMode = 'qa';
1699
1727
  let view = 'features';
1700
- let probeData = null; // { status, lastRun, intervalSec, nextRunAt, checks, configFound }
1728
+ let probeData = null; // { status, lastRun, intervalSec, nextRunAt, checks, configFound, checkResults, runningCheck }
1701
1729
  let probeRunning = false;
1730
+ let probeDetailCheck = null; // null = cards view, string = check name being viewed
1702
1731
  let searchQuery = '';
1703
1732
  let activeTypes = new Set();
1704
1733
  let activePanelKey = null;
@@ -3382,82 +3411,194 @@ async function loadProbeData() {
3382
3411
  try {
3383
3412
  const res = await fetch('/api/probe/status');
3384
3413
  probeData = await res.json();
3414
+ if (contextMode === 'probe') renderContent();
3385
3415
  } catch {}
3386
3416
  }
3387
3417
 
3418
+ function probeCheckStatus(checkName) {
3419
+ if (!probeData) return 'idle';
3420
+ if (probeData.runningCheck === checkName) return 'running';
3421
+ const r = probeData.checkResults?.[checkName];
3422
+ return r ? r.status : 'idle';
3423
+ }
3424
+
3425
+ function probeCheckResult(checkName) {
3426
+ return probeData?.checkResults?.[checkName] || null;
3427
+ }
3428
+
3429
+ function probeStatusIcon(status) {
3430
+ if (status === 'running') return `<span class="probe-spin">⏳</span>`;
3431
+ if (status === 'passed') return '✅';
3432
+ if (status === 'failed') return '❌';
3433
+ return '⏸';
3434
+ }
3435
+
3436
+ function probeStatusBadge(status) {
3437
+ if (status === 'running') return `<span class="probe-status-badge probe-status-running"><span class="probe-spin">⏳</span> Выполняется</span>`;
3438
+ if (status === 'passed') return `<span class="probe-status-badge probe-status-passed">✅ Passed</span>`;
3439
+ if (status === 'failed') return `<span class="probe-status-badge probe-status-failed">❌ Failed</span>`;
3440
+ return `<span class="probe-status-badge probe-status-idle">⏸ Не запускался</span>`;
3441
+ }
3442
+
3388
3443
  function renderProbePanel(c) {
3444
+ if (probeDetailCheck !== null) { renderProbeDetail(c); return; }
3445
+
3389
3446
  const d = probeData;
3390
- const statusColor = !d ? 'var(--muted)' : d.status === 'running' ? 'var(--yellow)' : d.status === 'scheduled' ? 'var(--green)' : 'var(--muted)';
3391
- const statusLabel = !d ? '—' : d.status === 'running' ? '● running' : d.status === 'scheduled' ? '● scheduled' : '○ idle';
3392
-
3393
- const lastRunHtml = d && d.lastRun ? (() => {
3394
- const r = d.lastRun;
3395
- const allPassed = r.failed === 0;
3396
- const rows = r.results.map(res => {
3397
- const ok = res.status === 'passed';
3398
- return `<div class="probe-check-row">
3399
- <span class="probe-check-icon">${ok ? '✅' : '❌'}</span>
3400
- <span class="probe-check-name">${escapeHtml(res.check)}</span>
3401
- <span class="probe-check-dur" style="color:var(--muted)">${res.durationMs}ms</span>
3402
- ${!ok && res.error ? `<div class="probe-check-error">${escapeHtml(res.error)}</div>` : ''}
3403
- ${!ok && res.screenshotPath ? `<div class="probe-check-error" style="color:var(--muted)">📸 ${escapeHtml(res.screenshotPath)}</div>` : ''}
3404
- </div>`;
3405
- }).join('');
3406
- return `
3407
- <div class="probe-section">
3408
- <div class="probe-section-title">Последний прогон <span style="color:var(--muted);font-weight:400;font-size:11px">${r.timestamp ? new Date(r.timestamp).toLocaleTimeString() : ''}</span>
3409
- <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>
3410
- </div>
3411
- <div class="probe-checks">${rows}</div>
3412
- </div>`;
3413
- })() : `<div style="color:var(--muted);font-size:13px;padding:16px 0">Ещё не запускался</div>`;
3447
+ const isRunning = d?.status === 'running';
3448
+ const isScheduled = d?.status === 'scheduled';
3414
3449
 
3415
3450
  const intervalOptions = [60, 300, 600, 1800, 3600].map(s => {
3416
3451
  const label = s < 3600 ? `каждые ${s / 60} мин` : `каждый час`;
3417
- const sel = d && d.intervalSec === s ? ' selected' : '';
3452
+ const sel = d?.intervalSec === s ? ' selected' : '';
3418
3453
  return `<option value="${s}"${sel}>${label}</option>`;
3419
3454
  }).join('');
3420
3455
 
3421
- const isScheduled = d && d.status === 'scheduled';
3422
- const isRunning = d && d.status === 'running';
3456
+ const checks = d?.checks || [];
3457
+
3458
+ const cards = checks.map(ch => {
3459
+ const status = probeCheckStatus(ch.name);
3460
+ const result = probeCheckResult(ch.name);
3461
+ const dur = result ? `${(result.durationMs / 1000).toFixed(1)}s` : '';
3462
+ const typeLabel = ch.type === 'file' ? 'playwright' : 'dsl';
3463
+ const stepsCount = ch.type === 'dsl' ? `${ch.steps.length} шагов` : ch.file?.split('/').pop() || '';
3464
+ return `
3465
+ <div class="probe-card ${status}" onclick="probeOpenDetail(${JSON.stringify(ch.name)})">
3466
+ <div class="probe-card-top">
3467
+ <div class="probe-card-name">${escapeHtml(ch.name)}</div>
3468
+ <div class="probe-card-icon">${probeStatusIcon(status)}</div>
3469
+ </div>
3470
+ <div class="probe-card-meta">
3471
+ <span class="probe-badge">${typeLabel}</span>
3472
+ <span class="probe-card-dur" style="color:var(--muted);font-size:11px">${escapeHtml(stepsCount)}</span>
3473
+ ${dur ? `<span class="probe-card-dur">${dur}</span>` : ''}
3474
+ </div>
3475
+ ${result?.error ? `<div class="probe-card-err">${escapeHtml(result.error)}</div>` : ''}
3476
+ <div class="probe-card-footer">
3477
+ <button class="probe-btn probe-btn-run" style="font-size:11px;padding:4px 10px"
3478
+ onclick="event.stopPropagation();probeRunOne(${JSON.stringify(ch.name)})"
3479
+ ${status === 'running' ? 'disabled' : ''}>▶ Run</button>
3480
+ </div>
3481
+ </div>`;
3482
+ }).join('');
3483
+
3484
+ const lastTs = d?.lastRun?.timestamp ? `· последний запуск ${new Date(d.lastRun.timestamp).toLocaleTimeString()}` : '';
3485
+ const passedCount = checks.filter(ch => probeCheckStatus(ch.name) === 'passed').length;
3486
+ const failedCount = checks.filter(ch => probeCheckStatus(ch.name) === 'failed').length;
3487
+ const summary = checks.length
3488
+ ? `${failedCount > 0 ? `<span style="color:var(--red)">❌ ${failedCount}</span> · ` : ''}${passedCount > 0 ? `<span style="color:var(--green)">✅ ${passedCount}</span> · ` : ''}${checks.length} checks ${lastTs}`
3489
+ : '';
3423
3490
 
3424
3491
  c.innerHTML = `
3425
3492
  <div class="probe-container">
3426
3493
  <div class="probe-header">
3427
3494
  <div>
3428
3495
  <div class="probe-title">🔭 Probe monitoring</div>
3429
- ${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>`
3430
- : `<div style="color:var(--yellow);font-size:12px">⚠️ Задайте домен стенда в Настройках</div>`}
3496
+ <div style="font-size:12px;color:var(--muted);margin-top:3px">
3497
+ ${d?.effectiveTarget ? escapeHtml(d.effectiveTarget) + ' · ' : ''}${summary || '<span style="color:#e3b341">⚠️ Задайте домен в Настройках</span>'}
3498
+ </div>
3431
3499
  </div>
3432
3500
  <button class="probe-btn" onclick="openProbeSettingsModal()">⚙️ Настройки</button>
3433
3501
  </div>
3434
3502
 
3435
- <div class="probe-section">
3503
+ <div class="probe-section" style="margin-bottom:0">
3436
3504
  <div class="probe-controls">
3437
3505
  <button class="probe-btn probe-btn-run" onclick="probeRunNow()" ${isRunning ? 'disabled' : ''}>
3438
- ${isRunning ? ' Выполняется…' : '▶ Run Now'}
3506
+ ${isRunning ? '<span class="probe-spin">⏳</span> Выполняется…' : '▶ Run All'}
3439
3507
  </button>
3440
3508
  <div class="probe-schedule-row">
3441
- <select id="probeIntervalSelect" class="probe-select">
3442
- ${intervalOptions}
3443
- </select>
3509
+ <select id="probeIntervalSelect" class="probe-select">${intervalOptions}</select>
3444
3510
  ${isScheduled
3445
3511
  ? `<button class="probe-btn probe-btn-stop" onclick="probeScheduleStop()">⏹ Стоп</button>`
3446
- : `<button class="probe-btn probe-btn-start" onclick="probeScheduleStart()">🕐 По расписанию</button>`
3447
- }
3512
+ : `<button class="probe-btn probe-btn-start" onclick="probeScheduleStart()">🕐 По расписанию</button>`}
3513
+ </div>
3514
+ ${isScheduled && d.nextRunAt ? `<span style="font-size:11px;color:var(--muted)">след. ${new Date(d.nextRunAt).toLocaleTimeString()}</span>` : ''}
3515
+ </div>
3516
+ </div>
3517
+
3518
+ ${checks.length
3519
+ ? `<div class="probe-cards">${cards}</div>`
3520
+ : `<div style="color:var(--muted);font-size:13px;padding:24px 0">Нет проверок в probe.config.yml</div>`}
3521
+ </div>`;
3522
+ }
3523
+
3524
+ function probeOpenDetail(checkName) {
3525
+ probeDetailCheck = checkName;
3526
+ renderContent();
3527
+ }
3528
+
3529
+ function renderProbeDetail(c) {
3530
+ const d = probeData;
3531
+ const checks = d?.checks || [];
3532
+ const ch = checks.find(x => x.name === probeDetailCheck);
3533
+ if (!ch) { probeDetailCheck = null; renderProbePanel(c); return; }
3534
+
3535
+ const status = probeCheckStatus(ch.name);
3536
+ const result = probeCheckResult(ch.name);
3537
+ const isRunning = status === 'running';
3538
+
3539
+ const stepsHtml = ch.type === 'dsl' && ch.steps?.length
3540
+ ? `<div style="font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:8px">Шаги</div>
3541
+ <div class="probe-steps-list">
3542
+ ${ch.steps.map(s => `
3543
+ <div class="probe-step-row">
3544
+ <span class="probe-step-type">${escapeHtml(s.type)}</span>
3545
+ <span class="probe-step-val">${escapeHtml(s.value)}</span>
3546
+ </div>`).join('')}
3547
+ </div>` : '';
3548
+
3549
+ const fileHtml = ch.type === 'file'
3550
+ ? `<div style="font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-bottom:6px">Файл теста</div>
3551
+ <code style="font-size:12px;color:var(--blue);background:var(--bg);padding:6px 10px;border-radius:4px;display:block">${escapeHtml(ch.file || '')}</code>` : '';
3552
+
3553
+ const outputHtml = result?.output
3554
+ ? `<div style="font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-top:20px;margin-bottom:6px">Вывод</div>
3555
+ <div class="probe-output-box">${escapeHtml(result.output)}</div>` : '';
3556
+
3557
+ const errorHtml = result?.error && !result.output
3558
+ ? `<div style="font-size:12px;font-weight:600;color:var(--muted);text-transform:uppercase;letter-spacing:.5px;margin-top:20px;margin-bottom:6px">Ошибка</div>
3559
+ <div class="probe-output-box" style="color:var(--red)">${escapeHtml(result.error)}</div>` : '';
3560
+
3561
+ c.innerHTML = `
3562
+ <div class="probe-detail">
3563
+ <div class="probe-back" onclick="probeDetailCheck=null;renderContent()">← Назад к проверкам</div>
3564
+
3565
+ <div class="probe-detail-header">
3566
+ <div>
3567
+ <div class="probe-detail-title">${escapeHtml(ch.name)}</div>
3568
+ <div class="probe-detail-meta">
3569
+ <span class="probe-badge">${ch.type === 'file' ? 'playwright' : 'dsl'}</span>
3570
+ ${probeStatusBadge(status)}
3571
+ ${result ? `<span style="color:var(--muted)">${(result.durationMs / 1000).toFixed(1)}s</span>` : ''}
3572
+ ${result ? `<span style="color:var(--muted)">${new Date().toLocaleTimeString()}</span>` : ''}
3448
3573
  </div>
3449
3574
  </div>
3450
- ${isScheduled && d.nextRunAt ? `<div style="font-size:11px;color:var(--muted);margin-top:6px">Следующий запуск: ${new Date(d.nextRunAt).toLocaleTimeString()}</div>` : ''}
3575
+ <button class="probe-btn probe-btn-run" style="flex-shrink:0" onclick="probeRunOne(${JSON.stringify(ch.name)})" ${isRunning ? 'disabled' : ''}>
3576
+ ${isRunning ? '<span class="probe-spin">⏳</span> Выполняется…' : '▶ Run'}
3577
+ </button>
3451
3578
  </div>
3452
3579
 
3453
- ${lastRunHtml}
3580
+ ${stepsHtml}
3581
+ ${fileHtml}
3582
+ ${outputHtml}
3583
+ ${errorHtml}
3454
3584
  </div>`;
3455
3585
  }
3456
3586
 
3457
3587
  async function probeRunNow() {
3458
3588
  probeRunning = true;
3459
3589
  renderContent();
3460
- try { await fetch('/api/probe/run', { method: 'POST' }); } catch {}
3590
+ try { await fetch('/api/probe/run', { method: 'POST', headers: {'Content-Type':'application/json'}, body: '{}' }); } catch {}
3591
+ }
3592
+
3593
+ async function probeRunOne(checkName) {
3594
+ try {
3595
+ await fetch('/api/probe/run', {
3596
+ method: 'POST',
3597
+ headers: { 'Content-Type': 'application/json' },
3598
+ body: JSON.stringify({ checkName }),
3599
+ });
3600
+ renderContent();
3601
+ } catch {}
3461
3602
  }
3462
3603
 
3463
3604
  async function probeScheduleStart() {
@@ -8007,11 +8148,29 @@ function connectSSE() {
8007
8148
  if (contextMode === 'load') { renderSidebar(); renderContent(); }
8008
8149
  });
8009
8150
 
8010
- es.addEventListener('probe-run-started', () => {
8151
+ es.addEventListener('probe-run-started', (e) => {
8152
+ const payload = JSON.parse(e.data || '{}');
8011
8153
  if (!probeData) probeData = {};
8012
8154
  probeData.status = 'running';
8155
+ probeData.runningCheck = null;
8013
8156
  probeRunning = true;
8014
- if (view === 'probe') renderContent();
8157
+ if (contextMode === 'probe') renderContent();
8158
+ });
8159
+
8160
+ es.addEventListener('probe-check-started', (e) => {
8161
+ const payload = JSON.parse(e.data || '{}');
8162
+ if (!probeData) probeData = {};
8163
+ probeData.runningCheck = payload.checkName;
8164
+ if (contextMode === 'probe') renderContent();
8165
+ });
8166
+
8167
+ es.addEventListener('probe-check-done', (e) => {
8168
+ const result = JSON.parse(e.data || '{}');
8169
+ if (!probeData) probeData = {};
8170
+ if (!probeData.checkResults) probeData.checkResults = {};
8171
+ probeData.checkResults[result.check] = result;
8172
+ probeData.runningCheck = null;
8173
+ if (contextMode === 'probe') renderContent();
8015
8174
  });
8016
8175
 
8017
8176
  es.addEventListener('probe-run-done', (e) => {
@@ -8019,8 +8178,9 @@ function connectSSE() {
8019
8178
  if (!probeData) probeData = {};
8020
8179
  if (payload.results) probeData.lastRun = payload;
8021
8180
  probeData.status = probeData.intervalSec ? 'scheduled' : 'idle';
8181
+ probeData.runningCheck = null;
8022
8182
  probeRunning = false;
8023
- if (view === 'probe') renderContent();
8183
+ if (contextMode === 'probe') renderContent();
8024
8184
  });
8025
8185
 
8026
8186
  es.addEventListener('probe-scheduled', (e) => {
@@ -8029,7 +8189,7 @@ function connectSSE() {
8029
8189
  probeData.status = payload.status;
8030
8190
  probeData.intervalSec = payload.intervalSec;
8031
8191
  probeData.nextRunAt = payload.nextRunAt;
8032
- if (view === 'probe') renderContent();
8192
+ if (contextMode === 'probe') renderContent();
8033
8193
  });
8034
8194
 
8035
8195
  es.onerror = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.179",
3
+ "version": "0.3.181",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {