viberadar 0.3.4 → 0.3.6
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 +4 -1
- package/dist/cli.js.map +1 -1
- package/dist/scanner/index.d.ts +11 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +90 -9
- package/dist/scanner/index.js.map +1 -1
- package/dist/server/index.d.ts +5 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +418 -9
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +616 -36
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -46,6 +46,43 @@
|
|
|
46
46
|
.header-project { margin-left: auto; font-size: 13px; color: var(--muted); }
|
|
47
47
|
.header-time { font-size: 12px; color: var(--dim); }
|
|
48
48
|
|
|
49
|
+
/* ── Coverage button ─────────────────────────────────────────────────────── */
|
|
50
|
+
#covBtn {
|
|
51
|
+
padding: 5px 12px;
|
|
52
|
+
background: var(--bg);
|
|
53
|
+
border: 1px solid var(--border);
|
|
54
|
+
border-radius: 6px;
|
|
55
|
+
color: var(--muted);
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
cursor: pointer;
|
|
58
|
+
display: flex;
|
|
59
|
+
align-items: center;
|
|
60
|
+
gap: 5px;
|
|
61
|
+
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
62
|
+
white-space: nowrap;
|
|
63
|
+
}
|
|
64
|
+
#covBtn:hover:not(:disabled) { background: var(--bg-hover); color: var(--text); border-color: var(--dim); }
|
|
65
|
+
#covBtn:disabled { cursor: not-allowed; opacity: 0.7; }
|
|
66
|
+
#covBtn.cov-running { color: var(--yellow); border-color: var(--yellow); }
|
|
67
|
+
#covBtn.cov-error { color: var(--red); border-color: var(--red); }
|
|
68
|
+
#covBtn.cov-done { color: var(--green); border-color: var(--green); }
|
|
69
|
+
#termBtn {
|
|
70
|
+
padding: 5px 12px;
|
|
71
|
+
background: var(--bg);
|
|
72
|
+
border: 1px solid var(--border);
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
color: var(--muted);
|
|
75
|
+
font-size: 12px;
|
|
76
|
+
cursor: pointer;
|
|
77
|
+
display: flex;
|
|
78
|
+
align-items: center;
|
|
79
|
+
gap: 5px;
|
|
80
|
+
transition: background 0.1s, color 0.1s, border-color 0.1s;
|
|
81
|
+
white-space: nowrap;
|
|
82
|
+
}
|
|
83
|
+
#termBtn:hover { background: var(--bg-hover); color: var(--text); border-color: var(--dim); }
|
|
84
|
+
#termBtn.term-active { color: var(--accent); border-color: var(--accent); }
|
|
85
|
+
|
|
49
86
|
/* ── Stats bar ───────────────────────────────────────────────────────────── */
|
|
50
87
|
.stats-bar {
|
|
51
88
|
display: flex;
|
|
@@ -379,6 +416,45 @@
|
|
|
379
416
|
letter-spacing: 0.5px; color: var(--muted);
|
|
380
417
|
margin: 14px 0 6px;
|
|
381
418
|
}
|
|
419
|
+
|
|
420
|
+
/* ── Test-type cards inside feature detail ───────────────────────────────── */
|
|
421
|
+
.test-type-grid {
|
|
422
|
+
display: grid;
|
|
423
|
+
grid-template-columns: repeat(4, 1fr);
|
|
424
|
+
gap: 10px;
|
|
425
|
+
margin-bottom: 20px;
|
|
426
|
+
}
|
|
427
|
+
.test-type-card {
|
|
428
|
+
background: var(--bg-card);
|
|
429
|
+
border: 1px solid var(--border);
|
|
430
|
+
border-radius: 8px;
|
|
431
|
+
padding: 14px 16px;
|
|
432
|
+
cursor: pointer;
|
|
433
|
+
transition: background 0.15s, border-color 0.15s;
|
|
434
|
+
position: relative;
|
|
435
|
+
overflow: hidden;
|
|
436
|
+
}
|
|
437
|
+
.test-type-card:hover { background: var(--bg-hover); border-color: var(--dim); }
|
|
438
|
+
.test-type-card .tt-accent {
|
|
439
|
+
position: absolute; top: 0; left: 0; right: 0; height: 3px;
|
|
440
|
+
}
|
|
441
|
+
.test-type-card .tt-label {
|
|
442
|
+
font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px;
|
|
443
|
+
color: var(--muted); margin-bottom: 6px; margin-top: 2px;
|
|
444
|
+
}
|
|
445
|
+
.test-type-card .tt-count {
|
|
446
|
+
font-size: 26px; font-weight: 700; line-height: 1;
|
|
447
|
+
margin-bottom: 4px;
|
|
448
|
+
}
|
|
449
|
+
.test-type-card .tt-sub {
|
|
450
|
+
font-size: 11px; color: var(--dim);
|
|
451
|
+
}
|
|
452
|
+
.test-type-card.tt-empty .tt-count { color: var(--dim); }
|
|
453
|
+
.test-type-card.tt-empty { opacity: 0.7; }
|
|
454
|
+
.test-type-card.tt-active {
|
|
455
|
+
background: var(--bg-hover);
|
|
456
|
+
border-width: 2px;
|
|
457
|
+
}
|
|
382
458
|
.file-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
383
459
|
.file-row {
|
|
384
460
|
display: grid;
|
|
@@ -397,6 +473,90 @@
|
|
|
397
473
|
.file-row-name { font-weight: 500; word-break: break-all; }
|
|
398
474
|
.file-row-dir { font-size: 11px; color: var(--dim); text-align: right; word-break: break-word; }
|
|
399
475
|
|
|
476
|
+
/* ── Agent setup banner ──────────────────────────────────────────────────── */
|
|
477
|
+
.agent-setup-banner {
|
|
478
|
+
background: linear-gradient(135deg, #161b22 0%, #1c2230 100%);
|
|
479
|
+
border: 1px solid var(--blue);
|
|
480
|
+
border-radius: 10px;
|
|
481
|
+
padding: 24px;
|
|
482
|
+
margin-bottom: 20px;
|
|
483
|
+
text-align: center;
|
|
484
|
+
}
|
|
485
|
+
.agent-setup-banner h3 { font-size: 15px; margin-bottom: 6px; }
|
|
486
|
+
.agent-setup-banner p { font-size: 12px; color: var(--muted); margin-bottom: 16px; }
|
|
487
|
+
.agent-choices { display: flex; gap: 10px; justify-content: center; }
|
|
488
|
+
.agent-choice-btn {
|
|
489
|
+
padding: 10px 24px;
|
|
490
|
+
border-radius: 8px;
|
|
491
|
+
border: 1px solid var(--border);
|
|
492
|
+
background: var(--bg);
|
|
493
|
+
color: var(--text);
|
|
494
|
+
font-size: 13px;
|
|
495
|
+
font-weight: 600;
|
|
496
|
+
cursor: pointer;
|
|
497
|
+
transition: background 0.15s, border-color 0.15s, transform 0.1s;
|
|
498
|
+
}
|
|
499
|
+
.agent-choice-btn:hover { background: var(--bg-hover); border-color: var(--blue); transform: translateY(-1px); }
|
|
500
|
+
|
|
501
|
+
/* ── Agent card button ───────────────────────────────────────────────────── */
|
|
502
|
+
.agent-card-btn {
|
|
503
|
+
margin-top: 10px;
|
|
504
|
+
padding: 5px 10px;
|
|
505
|
+
background: var(--bg);
|
|
506
|
+
border: 1px solid var(--border);
|
|
507
|
+
border-radius: 5px;
|
|
508
|
+
color: var(--blue);
|
|
509
|
+
font-size: 11px;
|
|
510
|
+
font-weight: 600;
|
|
511
|
+
cursor: pointer;
|
|
512
|
+
transition: background 0.1s, border-color 0.1s;
|
|
513
|
+
width: 100%;
|
|
514
|
+
text-align: left;
|
|
515
|
+
}
|
|
516
|
+
.agent-card-btn:hover { background: var(--bg-hover); border-color: var(--blue); }
|
|
517
|
+
.agent-card-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
518
|
+
|
|
519
|
+
/* ── Agent terminal panel ────────────────────────────────────────────────── */
|
|
520
|
+
.agent-panel {
|
|
521
|
+
position: fixed;
|
|
522
|
+
bottom: 0; left: 0; right: 0;
|
|
523
|
+
height: 280px;
|
|
524
|
+
background: #090d13;
|
|
525
|
+
border-top: 1px solid var(--border);
|
|
526
|
+
transform: translateY(100%);
|
|
527
|
+
transition: transform 0.25s ease;
|
|
528
|
+
z-index: 200;
|
|
529
|
+
display: flex;
|
|
530
|
+
flex-direction: column;
|
|
531
|
+
}
|
|
532
|
+
.agent-panel.open { transform: translateY(0); }
|
|
533
|
+
.agent-panel-header {
|
|
534
|
+
display: flex;
|
|
535
|
+
align-items: center;
|
|
536
|
+
gap: 10px;
|
|
537
|
+
padding: 8px 16px;
|
|
538
|
+
background: var(--bg-card);
|
|
539
|
+
border-bottom: 1px solid var(--border);
|
|
540
|
+
flex-shrink: 0;
|
|
541
|
+
}
|
|
542
|
+
.agent-panel-title { font-size: 13px; font-weight: 600; flex: 1; }
|
|
543
|
+
.agent-panel-status { font-size: 11px; color: var(--muted); }
|
|
544
|
+
.agent-panel-close {
|
|
545
|
+
background: none; border: none; color: var(--muted); cursor: pointer;
|
|
546
|
+
font-size: 14px; padding: 3px 6px; border-radius: 4px; line-height: 1;
|
|
547
|
+
}
|
|
548
|
+
.agent-panel-close:hover { background: var(--border); color: var(--text); }
|
|
549
|
+
.agent-terminal {
|
|
550
|
+
flex: 1;
|
|
551
|
+
overflow-y: auto;
|
|
552
|
+
padding: 10px 16px;
|
|
553
|
+
font-family: 'Consolas', 'Menlo', 'Courier New', monospace;
|
|
554
|
+
font-size: 12px;
|
|
555
|
+
line-height: 1.5;
|
|
556
|
+
}
|
|
557
|
+
.agent-line { color: #c9d1d9; }
|
|
558
|
+
.agent-line.err { color: var(--red); }
|
|
559
|
+
|
|
400
560
|
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
401
561
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
402
562
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
@@ -409,6 +569,8 @@
|
|
|
409
569
|
<h1>VibeRadar</h1>
|
|
410
570
|
<span class="header-project" id="projectName">—</span>
|
|
411
571
|
<span class="header-time" id="scannedAt"></span>
|
|
572
|
+
<button id="covBtn" onclick="runCoverage()" title="Запустить тесты с coverage">🧪 Coverage</button>
|
|
573
|
+
<button id="termBtn" onclick="toggleAgentPanel()" title="Показать/скрыть терминал агента">📟 Terminal</button>
|
|
412
574
|
<span id="liveDot" title="Connecting…" style="
|
|
413
575
|
width:8px; height:8px; border-radius:50%;
|
|
414
576
|
background:var(--dim); display:inline-block;
|
|
@@ -439,6 +601,15 @@
|
|
|
439
601
|
<div id="panelContent"></div>
|
|
440
602
|
</div>
|
|
441
603
|
|
|
604
|
+
<div class="agent-panel" id="agentPanel">
|
|
605
|
+
<div class="agent-panel-header">
|
|
606
|
+
<span class="agent-panel-title" id="agentPanelTitle">🤖 Agent</span>
|
|
607
|
+
<span class="agent-panel-status" id="agentPanelStatus">running…</span>
|
|
608
|
+
<button class="agent-panel-close" onclick="closeAgentPanel()">✕</button>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="agent-terminal" id="agentTerminal"></div>
|
|
611
|
+
</div>
|
|
612
|
+
|
|
442
613
|
<script>
|
|
443
614
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
444
615
|
let D = null;
|
|
@@ -446,7 +617,79 @@ let view = 'features';
|
|
|
446
617
|
let searchQuery = '';
|
|
447
618
|
let activeTypes = new Set();
|
|
448
619
|
let activePanelKey = null;
|
|
449
|
-
let drillFeatureKey = null;
|
|
620
|
+
let drillFeatureKey = null; // null = grid, '__unmapped__' = unmapped, string = feature key
|
|
621
|
+
let drillTestType = null; // null = feature overview, 'unit'|'integration'|'e2e' = test type drill
|
|
622
|
+
let coverageRunning = false;
|
|
623
|
+
let coverageHasError = false;
|
|
624
|
+
|
|
625
|
+
// ─── Coverage button ───────────────────────────────────────────────────────────
|
|
626
|
+
function updateCovBtn() {
|
|
627
|
+
const btn = document.getElementById('covBtn');
|
|
628
|
+
if (!btn) return;
|
|
629
|
+
btn.className = '';
|
|
630
|
+
btn.disabled = coverageRunning;
|
|
631
|
+
if (coverageRunning) {
|
|
632
|
+
btn.className = 'cov-running';
|
|
633
|
+
btn.textContent = '⏳ Running...';
|
|
634
|
+
} else if (coverageHasError) {
|
|
635
|
+
btn.className = 'cov-error';
|
|
636
|
+
btn.textContent = '❌ Coverage failed';
|
|
637
|
+
} else {
|
|
638
|
+
btn.textContent = '🧪 Coverage';
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async function runCoverage() {
|
|
643
|
+
if (coverageRunning) return;
|
|
644
|
+
await fetch('/api/run-coverage', { method: 'POST' });
|
|
645
|
+
// SSE events will update state
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
// ─── Agent ────────────────────────────────────────────────────────────────────
|
|
649
|
+
let agentRunning = false;
|
|
650
|
+
|
|
651
|
+
async function setAgent(agent) {
|
|
652
|
+
await fetch('/api/set-agent', {
|
|
653
|
+
method: 'POST',
|
|
654
|
+
headers: { 'Content-Type': 'application/json' },
|
|
655
|
+
body: JSON.stringify({ agent }),
|
|
656
|
+
});
|
|
657
|
+
// scheduleRescan will fire → data-updated → D.agent updates
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
async function runAgentTask(task, featureKey) {
|
|
661
|
+
if (agentRunning) return;
|
|
662
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
663
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
664
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
665
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
666
|
+
await fetch('/api/run-agent', {
|
|
667
|
+
method: 'POST',
|
|
668
|
+
headers: { 'Content-Type': 'application/json' },
|
|
669
|
+
body: JSON.stringify({ task, featureKey }),
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
function closeAgentPanel() {
|
|
674
|
+
document.getElementById('agentPanel').classList.remove('open');
|
|
675
|
+
document.getElementById('termBtn').classList.remove('term-active');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function toggleAgentPanel() {
|
|
679
|
+
const panel = document.getElementById('agentPanel');
|
|
680
|
+
const btn = document.getElementById('termBtn');
|
|
681
|
+
panel.classList.toggle('open');
|
|
682
|
+
btn.classList.toggle('term-active', panel.classList.contains('open'));
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
function appendTerminalLine(line, isError) {
|
|
686
|
+
const term = document.getElementById('agentTerminal');
|
|
687
|
+
const el = document.createElement('div');
|
|
688
|
+
el.className = 'agent-line' + (isError ? ' err' : '');
|
|
689
|
+
el.textContent = line;
|
|
690
|
+
term.appendChild(el);
|
|
691
|
+
term.scrollTop = term.scrollHeight;
|
|
692
|
+
}
|
|
450
693
|
|
|
451
694
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
452
695
|
const TYPE_COLORS = {
|
|
@@ -480,9 +723,19 @@ function pluralFiles(n) {
|
|
|
480
723
|
// ─── Init ─────────────────────────────────────────────────────────────────────
|
|
481
724
|
async function init() {
|
|
482
725
|
try {
|
|
483
|
-
const res = await
|
|
726
|
+
const [res, statusRes] = await Promise.all([
|
|
727
|
+
fetch('/api/data'),
|
|
728
|
+
fetch('/api/status').catch(() => null),
|
|
729
|
+
]);
|
|
484
730
|
D = await res.json();
|
|
485
731
|
|
|
732
|
+
if (statusRes) {
|
|
733
|
+
const status = await statusRes.json().catch(() => ({}));
|
|
734
|
+
coverageRunning = status.coverageRunning ?? false;
|
|
735
|
+
coverageHasError = status.coverageError ?? false;
|
|
736
|
+
}
|
|
737
|
+
updateCovBtn();
|
|
738
|
+
|
|
486
739
|
document.getElementById('projectName').textContent = D.projectName;
|
|
487
740
|
document.getElementById('scannedAt').textContent =
|
|
488
741
|
new Date(D.scannedAt).toLocaleTimeString();
|
|
@@ -511,7 +764,7 @@ function renderStats() {
|
|
|
511
764
|
|
|
512
765
|
let items;
|
|
513
766
|
if (D.hasConfig && D.features) {
|
|
514
|
-
const unmapped = src.filter(m => !m.featureKeys || m.featureKeys.length === 0).length;
|
|
767
|
+
const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
|
|
515
768
|
items = [
|
|
516
769
|
{ v: D.features.length, l: 'Features' },
|
|
517
770
|
{ v: src.length, l: 'Source Files' },
|
|
@@ -579,14 +832,24 @@ function renderSidebar() {
|
|
|
579
832
|
function renderContent() {
|
|
580
833
|
const c = document.getElementById('content');
|
|
581
834
|
if (view === 'features') {
|
|
582
|
-
drillFeatureKey
|
|
835
|
+
if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
|
|
836
|
+
else if (drillFeatureKey) renderFeatureDetail(c);
|
|
837
|
+
else renderFeatureCards(c);
|
|
583
838
|
} else {
|
|
584
839
|
renderModuleGrid(c);
|
|
585
840
|
}
|
|
586
841
|
}
|
|
587
842
|
|
|
843
|
+
function backToFeatureDetail() {
|
|
844
|
+
drillTestType = null;
|
|
845
|
+
activePanelKey = null;
|
|
846
|
+
document.getElementById('panel').classList.remove('open');
|
|
847
|
+
renderContent();
|
|
848
|
+
}
|
|
849
|
+
|
|
588
850
|
function backToFeatures() {
|
|
589
851
|
drillFeatureKey = null;
|
|
852
|
+
drillTestType = null;
|
|
590
853
|
activePanelKey = null;
|
|
591
854
|
document.getElementById('panel').classList.remove('open');
|
|
592
855
|
renderContent();
|
|
@@ -612,12 +875,25 @@ function renderFeatureCards(c) {
|
|
|
612
875
|
|
|
613
876
|
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
614
877
|
|
|
615
|
-
|
|
878
|
+
// Agent setup banner — shown when no agent configured
|
|
879
|
+
const setupBanner = !D.agent ? `
|
|
880
|
+
<div class="agent-setup-banner">
|
|
881
|
+
<h3>🤖 Выбери AI агента</h3>
|
|
882
|
+
<p>VibeRadar будет запускать его прямо из дашборда — писать тесты, разбирать unmapped и не только</p>
|
|
883
|
+
<div class="agent-choices">
|
|
884
|
+
<button class="agent-choice-btn" onclick="setAgent('claude')">⚡ Claude Code</button>
|
|
885
|
+
<button class="agent-choice-btn" onclick="setAgent('codex')">🟢 Codex (OpenAI)</button>
|
|
886
|
+
</div>
|
|
887
|
+
</div>` : '';
|
|
888
|
+
|
|
889
|
+
c.innerHTML = setupBanner + '<div class="features-grid" id="featGrid"></div>';
|
|
616
890
|
const grid = document.getElementById('featGrid');
|
|
617
891
|
|
|
618
892
|
list.forEach(f => {
|
|
619
893
|
const pct = f.fileCount > 0 ? Math.round(f.testedCount / f.fileCount * 100) : 0;
|
|
620
894
|
const isActive = activePanelKey === f.key;
|
|
895
|
+
const hasCov = f.coveragePct != null;
|
|
896
|
+
const covPct = hasCov ? Math.round(f.coveragePct) : null;
|
|
621
897
|
|
|
622
898
|
const card = document.createElement('div');
|
|
623
899
|
card.className = 'feature-card' + (isActive ? ' active' : '');
|
|
@@ -629,21 +905,51 @@ function renderFeatureCards(c) {
|
|
|
629
905
|
<span class="feature-file-count">${f.fileCount} ${pluralFiles(f.fileCount)}</span>
|
|
630
906
|
</div>
|
|
631
907
|
${f.description ? `<div class="feature-desc">${f.description}</div>` : ''}
|
|
908
|
+
${hasCov ? `
|
|
909
|
+
<div style="display:flex;align-items:center;gap:8px;margin-bottom:10px">
|
|
910
|
+
<span style="font-size:24px;font-weight:700;line-height:1;color:${covColor(covPct)}">${covPct}%</span>
|
|
911
|
+
<div style="flex:1">
|
|
912
|
+
<div style="font-size:10px;color:var(--muted);margin-bottom:3px;text-transform:uppercase;letter-spacing:0.4px">Coverage</div>
|
|
913
|
+
<div style="height:4px;background:var(--border);border-radius:2px;overflow:hidden">
|
|
914
|
+
<div style="width:${covPct}%;height:100%;background:${covColor(covPct)};border-radius:2px;transition:width 0.4s"></div>
|
|
915
|
+
</div>
|
|
916
|
+
</div>
|
|
917
|
+
</div>` : `
|
|
918
|
+
<div style="font-size:11px;color:var(--dim);margin-bottom:10px">
|
|
919
|
+
coverage: нет данных
|
|
920
|
+
</div>`}
|
|
632
921
|
<div class="feature-progress-wrap">
|
|
633
922
|
<div class="feature-progress-bar">
|
|
634
923
|
<div class="feature-progress-fill" style="width:${pct}%;background:${f.color}"></div>
|
|
635
924
|
</div>
|
|
636
|
-
<span class="feature-progress-label" style="color:${covColor(pct)}">${f.testedCount}/${f.fileCount}
|
|
925
|
+
<span class="feature-progress-label" style="color:${covColor(pct)}">${f.testedCount}/${f.fileCount} с тестами</span>
|
|
637
926
|
</div>
|
|
927
|
+
${D.agent && f.fileCount > f.testedCount ? `
|
|
928
|
+
<button class="agent-card-btn" data-task="write-tests" data-key="${f.key}">
|
|
929
|
+
▶ Написать тесты (${f.fileCount - f.testedCount} без тестов)
|
|
930
|
+
</button>` : ''}
|
|
638
931
|
</div>`;
|
|
639
|
-
card.onclick = () => {
|
|
932
|
+
card.onclick = (e) => {
|
|
933
|
+
if (e.target.closest('.agent-card-btn')) return; // don't drill on agent btn click
|
|
934
|
+
drillFeatureKey = f.key; activePanelKey = null;
|
|
935
|
+
document.getElementById('panel').classList.remove('open');
|
|
936
|
+
renderContent();
|
|
937
|
+
};
|
|
938
|
+
const agentBtn = card.querySelector('.agent-card-btn');
|
|
939
|
+
if (agentBtn) {
|
|
940
|
+
agentBtn.onclick = (e) => {
|
|
941
|
+
e.stopPropagation();
|
|
942
|
+
runAgentTask(agentBtn.dataset.task, agentBtn.dataset.key);
|
|
943
|
+
};
|
|
944
|
+
}
|
|
640
945
|
grid.appendChild(card);
|
|
641
946
|
});
|
|
642
947
|
|
|
643
948
|
// ── Unmapped card ──────────────────────────────────────────────────────────
|
|
644
949
|
if (!q) {
|
|
950
|
+
const infraSrc = D.modules.filter(m => m.type !== 'test' && m.isInfra);
|
|
645
951
|
const unmappedSrc = D.modules.filter(m =>
|
|
646
|
-
m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
|
|
952
|
+
m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)
|
|
647
953
|
);
|
|
648
954
|
if (unmappedSrc.length > 0) {
|
|
649
955
|
const isActive = activePanelKey === '__unmapped__';
|
|
@@ -651,6 +957,9 @@ function renderFeatureCards(c) {
|
|
|
651
957
|
card.className = 'feature-card' + (isActive ? ' active' : '');
|
|
652
958
|
card.style.borderStyle = 'dashed';
|
|
653
959
|
card.style.opacity = '0.75';
|
|
960
|
+
const infraNote = infraSrc.length > 0
|
|
961
|
+
? `<br><span style="color:var(--dim);font-size:11px">+ ${infraSrc.length} infra/system скрыты</span>`
|
|
962
|
+
: '';
|
|
654
963
|
card.innerHTML = `
|
|
655
964
|
<div class="feature-accent" style="background:var(--yellow)"></div>
|
|
656
965
|
<div class="feature-body">
|
|
@@ -658,7 +967,7 @@ function renderFeatureCards(c) {
|
|
|
658
967
|
<span style="color:var(--yellow)">⚠ Unmapped</span>
|
|
659
968
|
<span class="feature-file-count">${unmappedSrc.length} ${pluralFiles(unmappedSrc.length)}</span>
|
|
660
969
|
</div>
|
|
661
|
-
<div class="feature-desc">Файлы вне карты фич — не входят ни в одну
|
|
970
|
+
<div class="feature-desc">Файлы вне карты фич — не входят ни в одну фичу${infraNote}</div>
|
|
662
971
|
<div class="feature-progress-wrap">
|
|
663
972
|
<div class="feature-progress-bar">
|
|
664
973
|
<div class="feature-progress-fill" style="width:100%;background:var(--border)"></div>
|
|
@@ -666,12 +975,30 @@ function renderFeatureCards(c) {
|
|
|
666
975
|
<span class="feature-progress-label" style="color:var(--dim)">нет привязки</span>
|
|
667
976
|
</div>
|
|
668
977
|
</div>`;
|
|
669
|
-
card.onclick = () =>
|
|
978
|
+
card.onclick = () => {
|
|
979
|
+
drillFeatureKey = '__unmapped__';
|
|
980
|
+
activePanelKey = null;
|
|
981
|
+
document.getElementById('panel').classList.remove('open');
|
|
982
|
+
renderContent();
|
|
983
|
+
};
|
|
670
984
|
grid.appendChild(card);
|
|
671
985
|
}
|
|
672
986
|
}
|
|
673
987
|
}
|
|
674
988
|
|
|
989
|
+
function testTypeCard(type, label, icon, color, count, active) {
|
|
990
|
+
const empty = count === 0 && type !== 'source';
|
|
991
|
+
const subLabel = empty ? 'нет тестов' : (type === 'source' ? 'код приложения' : pluralFiles(count));
|
|
992
|
+
return `
|
|
993
|
+
<div class="test-type-card${empty ? ' tt-empty' : ''}${active ? ' tt-active' : ''}" data-testtype="${type}"
|
|
994
|
+
style="${active ? 'border-color:' + color : ''}">
|
|
995
|
+
<div class="tt-accent" style="background:${color}"></div>
|
|
996
|
+
<div class="tt-label">${icon} ${label}</div>
|
|
997
|
+
<div class="tt-count" style="color:${active || !empty ? color : 'var(--dim)'}">${count}</div>
|
|
998
|
+
<div class="tt-sub">${subLabel}</div>
|
|
999
|
+
</div>`;
|
|
1000
|
+
}
|
|
1001
|
+
|
|
675
1002
|
function renderFeatureDetail(c) {
|
|
676
1003
|
const feat = D.features.find(f => f.key === drillFeatureKey);
|
|
677
1004
|
if (!feat) { backToFeatures(); return; }
|
|
@@ -682,13 +1009,24 @@ function renderFeatureDetail(c) {
|
|
|
682
1009
|
const testedCount = src.filter(m => m.hasTests).length;
|
|
683
1010
|
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
684
1011
|
|
|
1012
|
+
const unitCount = feat.unitTestCount ?? tst.filter(m => m.testType === 'unit').length;
|
|
1013
|
+
const integrationCount = feat.integrationTestCount ?? tst.filter(m => m.testType === 'integration').length;
|
|
1014
|
+
const e2eCount = feat.e2eTestCount ?? tst.filter(m => m.testType === 'e2e').length;
|
|
1015
|
+
|
|
1016
|
+
// Determine what list to show based on active tab
|
|
1017
|
+
// null or 'source' → source files; test type → test files of that type
|
|
1018
|
+
const activeTab = drillTestType || 'source';
|
|
1019
|
+
const listFiles = activeTab === 'source' ? src : tst.filter(m => m.testType === activeTab);
|
|
1020
|
+
const isTestList = activeTab !== 'source';
|
|
1021
|
+
const meta = TEST_TYPE_META[activeTab];
|
|
1022
|
+
const listLabel = meta
|
|
1023
|
+
? `${meta.icon} ${meta.label} тесты (${listFiles.length})`
|
|
1024
|
+
: `📁 Файлы фичи (${listFiles.length})`;
|
|
1025
|
+
|
|
685
1026
|
const q = searchQuery.toLowerCase();
|
|
686
|
-
const
|
|
687
|
-
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
688
|
-
) : src;
|
|
689
|
-
const filteredTst = q ? tst.filter(m =>
|
|
1027
|
+
const filtered = q ? listFiles.filter(m =>
|
|
690
1028
|
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
691
|
-
) :
|
|
1029
|
+
) : listFiles;
|
|
692
1030
|
|
|
693
1031
|
c.innerHTML = `
|
|
694
1032
|
<div class="drill-header">
|
|
@@ -700,21 +1038,176 @@ function renderFeatureDetail(c) {
|
|
|
700
1038
|
<div class="drill-stats">
|
|
701
1039
|
<span>${src.length} файлов</span>
|
|
702
1040
|
<span style="color:${covColor(pct)}">${pct}% с тестами</span>
|
|
703
|
-
|
|
1041
|
+
${feat.coveragePct != null
|
|
1042
|
+
? `<span style="color:${covColor(Math.round(feat.coveragePct))};font-weight:700">${Math.round(feat.coveragePct)}% coverage</span>`
|
|
1043
|
+
: `<span style="color:var(--dim);font-size:11px" title="Нажми 🧪 Coverage в шапке">coverage: нет данных</span>`
|
|
1044
|
+
}
|
|
704
1045
|
</div>
|
|
705
1046
|
</div>
|
|
706
1047
|
${feat.description ? `<div class="drill-desc">${feat.description}</div>` : ''}
|
|
1048
|
+
|
|
1049
|
+
<div class="test-type-grid">
|
|
1050
|
+
${testTypeCard('source', 'Файлы', '📁', feat.color, src.length, activeTab === 'source')}
|
|
1051
|
+
${testTypeCard('unit', 'Unit', '🧪', '#e3b341', unitCount, activeTab === 'unit')}
|
|
1052
|
+
${testTypeCard('integration', 'Integration', '🔗', '#58a6ff', integrationCount, activeTab === 'integration')}
|
|
1053
|
+
${testTypeCard('e2e', 'E2E', '🎭', '#d2a8ff', e2eCount, activeTab === 'e2e')}
|
|
1054
|
+
</div>
|
|
1055
|
+
|
|
1056
|
+
<div class="drill-section-label">${listLabel}</div>
|
|
1057
|
+
<div class="file-rows" id="fileRows">
|
|
1058
|
+
${filtered.length === 0
|
|
1059
|
+
? `<div style="padding:20px;text-align:center;border:1px dashed var(--border);border-radius:8px;color:var(--dim);font-size:13px">
|
|
1060
|
+
${isTestList ? 'Нет тестов этого типа для данной фичи' : 'Нет файлов — возможно паттерны в конфиге не совпадают'}
|
|
1061
|
+
</div>`
|
|
1062
|
+
: filtered.map(m => fileRow(m, isTestList)).join('')
|
|
1063
|
+
}
|
|
1064
|
+
</div>`;
|
|
1065
|
+
|
|
1066
|
+
c.querySelectorAll('.test-type-card[data-testtype]').forEach(card => {
|
|
1067
|
+
card.onclick = () => {
|
|
1068
|
+
const type = card.dataset.testtype;
|
|
1069
|
+
drillTestType = (type === 'source') ? null : type; // 'source' tab = null state
|
|
1070
|
+
activePanelKey = null;
|
|
1071
|
+
document.getElementById('panel').classList.remove('open');
|
|
1072
|
+
renderContent();
|
|
1073
|
+
};
|
|
1074
|
+
});
|
|
1075
|
+
|
|
1076
|
+
c.querySelectorAll('.file-row[data-id]').forEach(row => {
|
|
1077
|
+
row.onclick = () => {
|
|
1078
|
+
const m = D.modules.find(m => m.id === row.dataset.id);
|
|
1079
|
+
if (m) openModulePanel(m);
|
|
1080
|
+
};
|
|
1081
|
+
});
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
const TEST_TYPE_META = {
|
|
1085
|
+
unit: { label: 'Unit', icon: '🧪', color: '#e3b341', desc: 'Изолированные тесты функций и модулей' },
|
|
1086
|
+
integration: { label: 'Integration', icon: '🔗', color: '#58a6ff', desc: 'Тесты с реальной БД и зависимостями' },
|
|
1087
|
+
e2e: { label: 'E2E', icon: '🎭', color: '#d2a8ff', desc: 'Сквозные тесты через браузер (Playwright)' },
|
|
1088
|
+
};
|
|
1089
|
+
|
|
1090
|
+
function renderTestTypeDetail(c) {
|
|
1091
|
+
const feat = D.features.find(f => f.key === drillFeatureKey);
|
|
1092
|
+
if (!feat) { backToFeatures(); return; }
|
|
1093
|
+
const meta = TEST_TYPE_META[drillTestType] || { label: drillTestType, icon: '🧪', color: '#58a6ff', desc: '' };
|
|
1094
|
+
|
|
1095
|
+
const tests = D.modules.filter(m =>
|
|
1096
|
+
m.type === 'test' &&
|
|
1097
|
+
m.testType === drillTestType &&
|
|
1098
|
+
m.featureKeys && m.featureKeys.includes(drillFeatureKey)
|
|
1099
|
+
);
|
|
1100
|
+
|
|
1101
|
+
const q = searchQuery.toLowerCase();
|
|
1102
|
+
const filtered = q ? tests.filter(m =>
|
|
1103
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
1104
|
+
) : tests;
|
|
1105
|
+
|
|
1106
|
+
c.innerHTML = `
|
|
1107
|
+
<div class="drill-header">
|
|
1108
|
+
<button class="back-btn" onclick="backToFeatureDetail()">← ${feat.label}</button>
|
|
1109
|
+
<div class="drill-title">
|
|
1110
|
+
<span>${meta.icon}</span>
|
|
1111
|
+
<span>${meta.label} тесты</span>
|
|
1112
|
+
</div>
|
|
1113
|
+
<div class="drill-stats">
|
|
1114
|
+
<span style="color:${meta.color}">${tests.length} ${pluralFiles(tests.length)}</span>
|
|
1115
|
+
</div>
|
|
1116
|
+
</div>
|
|
1117
|
+
<div style="font-size:12px;color:var(--dim);margin-bottom:16px">${meta.desc}</div>
|
|
1118
|
+
|
|
1119
|
+
<div class="file-rows" id="fileRows">
|
|
1120
|
+
${filtered.length === 0
|
|
1121
|
+
? `<div style="padding:24px;text-align:center;border:1px dashed var(--border);border-radius:8px">
|
|
1122
|
+
<div style="font-size:28px;margin-bottom:8px">${meta.icon}</div>
|
|
1123
|
+
<div style="font-size:14px;color:var(--muted);margin-bottom:4px">Нет ${meta.label} тестов для этой фичи</div>
|
|
1124
|
+
<div style="font-size:12px;color:var(--dim)">Добавь тесты в <code>${drillTestType === 'e2e' ? 'e2e/' : 'tests/'}</code></div>
|
|
1125
|
+
</div>`
|
|
1126
|
+
: filtered.map(m => fileRow(m, true)).join('')
|
|
1127
|
+
}
|
|
1128
|
+
</div>`;
|
|
1129
|
+
|
|
1130
|
+
c.querySelectorAll('.file-row[data-id]').forEach(row => {
|
|
1131
|
+
row.onclick = () => {
|
|
1132
|
+
const m = D.modules.find(m => m.id === row.dataset.id);
|
|
1133
|
+
if (m) openModulePanel(m);
|
|
1134
|
+
};
|
|
1135
|
+
});
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function renderUnmappedDetail(c) {
|
|
1139
|
+
const infraSrc = D.modules.filter(m => m.type !== 'test' && m.isInfra);
|
|
1140
|
+
const unmappedSrc = D.modules.filter(m =>
|
|
1141
|
+
m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)
|
|
1142
|
+
);
|
|
1143
|
+
|
|
1144
|
+
const q = searchQuery.toLowerCase();
|
|
1145
|
+
const filtered = q ? unmappedSrc.filter(m =>
|
|
1146
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
1147
|
+
) : unmappedSrc;
|
|
1148
|
+
|
|
1149
|
+
// Build prompt text
|
|
1150
|
+
const featureList = (D.features || []).map(f => ` • ${f.key} — ${f.label}`).join('\n');
|
|
1151
|
+
const plainList = unmappedSrc.map(m => '- ' + m.relativePath.replace(/\\/g, '/')).join('\n');
|
|
1152
|
+
const promptText =
|
|
1153
|
+
`В проекте ${unmappedSrc.length} файлов без привязки к фичам (unmapped).\n` +
|
|
1154
|
+
`\nДля каждого файла из списка реши:\n` +
|
|
1155
|
+
`1. Если файл относится к конкретной фиче → добавь его путь в "include" этой фичи в viberadar.config.json\n` +
|
|
1156
|
+
`2. Если это инфраструктура (утилиты, конфиги, middleware, типы, бутстрап) → добавь glob в массив "ignore"\n` +
|
|
1157
|
+
`3. Если файл явно бизнес-логика новой фичи → создай новую фичу\n` +
|
|
1158
|
+
`4. Если непонятно — пропусти\n` +
|
|
1159
|
+
`\nСуществующие фичи:\n${featureList}\n` +
|
|
1160
|
+
`\nФайлы:\n${plainList}`;
|
|
1161
|
+
|
|
1162
|
+
const infraNote = infraSrc.length > 0
|
|
1163
|
+
? `<span style="color:var(--dim);font-size:12px">+ ${infraSrc.length} infra/system скрыты (в ignore)</span>`
|
|
1164
|
+
: '';
|
|
1165
|
+
|
|
1166
|
+
c.innerHTML = `
|
|
1167
|
+
<div class="drill-header">
|
|
1168
|
+
<button class="back-btn" onclick="backToFeatures()">← Все фичи</button>
|
|
1169
|
+
<div class="drill-title">
|
|
1170
|
+
<span style="color:var(--yellow)">⚠</span>
|
|
1171
|
+
<span>Unmapped файлы</span>
|
|
1172
|
+
</div>
|
|
1173
|
+
<div class="drill-stats">
|
|
1174
|
+
<span style="color:var(--yellow)">${unmappedSrc.length} без привязки</span>
|
|
1175
|
+
${infraNote}
|
|
1176
|
+
</div>
|
|
1177
|
+
</div>
|
|
1178
|
+
<div style="padding:0 0 12px;display:flex;gap:8px;flex-wrap:wrap">
|
|
1179
|
+
${D.agent ? `<button id="runAgentUnmapped" style="
|
|
1180
|
+
padding:7px 14px; background:var(--blue); border:none;
|
|
1181
|
+
border-radius:6px; color:#000; font-size:12px; font-weight:700; cursor:pointer;
|
|
1182
|
+
">▶ Разобрать через ${D.agent === 'claude' ? 'Claude Code' : 'Codex'}</button>` : ''}
|
|
1183
|
+
<button id="copyUnmappedDrill" style="
|
|
1184
|
+
padding:7px 14px; background:var(--bg-card); border:1px solid var(--border);
|
|
1185
|
+
border-radius:6px; color:var(--blue); font-size:12px; cursor:pointer;
|
|
1186
|
+
">📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)</button>
|
|
1187
|
+
</div>
|
|
707
1188
|
<div class="file-rows" id="fileRows">
|
|
708
|
-
${
|
|
709
|
-
? '<div style="font-size:13px;color:var(--dim)"
|
|
710
|
-
:
|
|
1189
|
+
${filtered.length === 0
|
|
1190
|
+
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
1191
|
+
: filtered.map(m => fileRow(m)).join('')
|
|
711
1192
|
}
|
|
712
|
-
${filteredTst.length > 0 ? `
|
|
713
|
-
<div class="drill-section-label">Тест-файлы (${filteredTst.length})</div>
|
|
714
|
-
${filteredTst.map(m => fileRow(m, true)).join('')}
|
|
715
|
-
` : ''}
|
|
716
1193
|
</div>`;
|
|
717
1194
|
|
|
1195
|
+
const runAgentUnmappedBtn = document.getElementById('runAgentUnmapped');
|
|
1196
|
+
if (runAgentUnmappedBtn) {
|
|
1197
|
+
runAgentUnmappedBtn.onclick = () => runAgentTask('map-unmapped');
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
document.getElementById('copyUnmappedDrill').onclick = function() {
|
|
1201
|
+
navigator.clipboard.writeText(promptText).then(() => {
|
|
1202
|
+
this.textContent = '✅ Скопировано!';
|
|
1203
|
+
this.style.color = 'var(--green)';
|
|
1204
|
+
setTimeout(() => {
|
|
1205
|
+
this.textContent = `📋 Скопировать промпт для AI-агента (${unmappedSrc.length} файлов)`;
|
|
1206
|
+
this.style.color = 'var(--blue)';
|
|
1207
|
+
}, 3000);
|
|
1208
|
+
});
|
|
1209
|
+
};
|
|
1210
|
+
|
|
718
1211
|
c.querySelectorAll('.file-row[data-id]').forEach(row => {
|
|
719
1212
|
row.onclick = () => {
|
|
720
1213
|
const m = D.modules.find(m => m.id === row.dataset.id);
|
|
@@ -865,6 +1358,7 @@ function openModulePanel(m) {
|
|
|
865
1358
|
<span>Тесты</span>
|
|
866
1359
|
<span class="badge ${m.hasTests ? 'badge-green' : 'badge-red'}">${m.hasTests ? '✓ есть' : '✗ нет'}</span>
|
|
867
1360
|
</div>
|
|
1361
|
+
${m.testCount != null ? `<div class="detail-row"><span>Тест-кейсов</span><span style="color:var(--green);font-weight:600">${m.testCount}</span></div>` : ''}
|
|
868
1362
|
${m.testFile ? `<div class="detail-row"><span>Тест-файл</span><span class="detail-row-right">${m.testFile}</span></div>` : ''}
|
|
869
1363
|
${featureLabels ? `<div class="detail-row"><span>Фичи</span><span class="detail-row-right">${featureLabels}</span></div>` : ''}
|
|
870
1364
|
</div>
|
|
@@ -892,7 +1386,8 @@ function openModulePanel(m) {
|
|
|
892
1386
|
document.getElementById('panel').classList.add('open');
|
|
893
1387
|
}
|
|
894
1388
|
|
|
895
|
-
function openUnmappedPanel(files) {
|
|
1389
|
+
function openUnmappedPanel(files, infraFiles) {
|
|
1390
|
+
infraFiles = infraFiles || [];
|
|
896
1391
|
activePanelKey = '__unmapped__';
|
|
897
1392
|
renderContent();
|
|
898
1393
|
|
|
@@ -906,6 +1401,9 @@ function openUnmappedPanel(files) {
|
|
|
906
1401
|
});
|
|
907
1402
|
const dirs = Object.keys(byDir).sort();
|
|
908
1403
|
|
|
1404
|
+
// Build feature list for context
|
|
1405
|
+
const featureList = (D.features || []).map(f => ` • ${f.key} — ${f.label}`).join('\n');
|
|
1406
|
+
|
|
909
1407
|
// Build plain-text list for copying to AI agent
|
|
910
1408
|
const plainList = files
|
|
911
1409
|
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'))
|
|
@@ -913,26 +1411,38 @@ function openUnmappedPanel(files) {
|
|
|
913
1411
|
|
|
914
1412
|
const promptText =
|
|
915
1413
|
`В проекте ${files.length} файлов без привязки к фичам (unmapped).\n` +
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
1414
|
+
`\nДля каждого файла из списка реши:\n` +
|
|
1415
|
+
`1. Если файл относится к конкретной фиче → добавь его путь в "include" этой фичи в viberadar.config.json\n` +
|
|
1416
|
+
`2. Если это инфраструктура (утилиты, конфиги, middleware, типы, бутстрап) → добавь glob в массив "ignore"\n` +
|
|
1417
|
+
`3. Если файл явно бизнес-логика новой фичи → создай новую фичу\n` +
|
|
1418
|
+
`4. Если непонятно — пропусти\n` +
|
|
1419
|
+
`\nСуществующие фичи:\n${featureList}\n` +
|
|
1420
|
+
`\nФайлы:\n${plainList}`;
|
|
1421
|
+
|
|
1422
|
+
const infraNote = infraFiles.length > 0
|
|
1423
|
+
? `<div style="margin-bottom:12px;padding:8px 10px;background:var(--bg);border:1px solid var(--border);border-radius:6px;font-size:11px;color:var(--dim)">
|
|
1424
|
+
🔒 <b style="color:var(--text)">${infraFiles.length} infra/system файлов</b> скрыты (добавлены в <code>ignore</code>)<br>
|
|
1425
|
+
Они не считаются unmapped и не показываются в карте фич.
|
|
1426
|
+
</div>`
|
|
1427
|
+
: '';
|
|
919
1428
|
|
|
920
1429
|
document.getElementById('panelContent').innerHTML = `
|
|
921
1430
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
|
922
1431
|
<span style="font-size:16px">⚠</span>
|
|
923
1432
|
<div class="panel-title" style="color:var(--yellow)">Unmapped файлы</div>
|
|
924
1433
|
</div>
|
|
925
|
-
<div class="panel-subtitle">
|
|
926
|
-
Не входят ни в одну
|
|
927
|
-
Запусти <code style="color:var(--blue)">npx viberadar init</code> — агент предложит куда добавить.
|
|
1434
|
+
<div class="panel-subtitle" style="margin-bottom:12px">
|
|
1435
|
+
Не входят ни в одну фичу. Скопируй список и отправь AI-агенту.
|
|
928
1436
|
</div>
|
|
929
1437
|
|
|
1438
|
+
${infraNote}
|
|
1439
|
+
|
|
930
1440
|
<button id="copyUnmapped" style="
|
|
931
1441
|
width:100%; padding:8px 12px; margin-bottom:16px;
|
|
932
1442
|
background:var(--bg); border:1px solid var(--border);
|
|
933
1443
|
border-radius:6px; color:var(--blue); font-size:12px;
|
|
934
1444
|
cursor:pointer; text-align:left;
|
|
935
|
-
">📋 Скопировать
|
|
1445
|
+
">📋 Скопировать промпт для AI-агента (${files.length} файлов)</button>
|
|
936
1446
|
|
|
937
1447
|
${dirs.map(dir => `
|
|
938
1448
|
<div class="panel-section">
|
|
@@ -969,6 +1479,7 @@ document.querySelectorAll('.view-tab').forEach(tab => {
|
|
|
969
1479
|
if (tab.classList.contains('disabled')) return;
|
|
970
1480
|
view = tab.dataset.view;
|
|
971
1481
|
drillFeatureKey = null;
|
|
1482
|
+
drillTestType = null;
|
|
972
1483
|
activePanelKey = null;
|
|
973
1484
|
searchQuery = '';
|
|
974
1485
|
activeTypes.clear();
|
|
@@ -1010,11 +1521,8 @@ async function refreshData() {
|
|
|
1010
1521
|
// Re-render drill-down or re-open panel
|
|
1011
1522
|
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
1012
1523
|
if (panelOpen && activePanelKey) {
|
|
1013
|
-
if (
|
|
1014
|
-
|
|
1015
|
-
m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
|
|
1016
|
-
);
|
|
1017
|
-
openUnmappedPanel(unmapped);
|
|
1524
|
+
if (drillFeatureKey === '__unmapped__') {
|
|
1525
|
+
renderContent(); // already routes to renderUnmappedDetail
|
|
1018
1526
|
} else if (view === 'features' && D.features) {
|
|
1019
1527
|
openFeaturePanel(activePanelKey);
|
|
1020
1528
|
} else {
|
|
@@ -1042,6 +1550,78 @@ function connectSSE() {
|
|
|
1042
1550
|
refreshData();
|
|
1043
1551
|
});
|
|
1044
1552
|
|
|
1553
|
+
es.addEventListener('coverage-started', () => {
|
|
1554
|
+
coverageRunning = true;
|
|
1555
|
+
coverageHasError = false;
|
|
1556
|
+
updateCovBtn();
|
|
1557
|
+
});
|
|
1558
|
+
|
|
1559
|
+
es.addEventListener('coverage-done', () => {
|
|
1560
|
+
coverageRunning = false;
|
|
1561
|
+
coverageHasError = false;
|
|
1562
|
+
updateCovBtn();
|
|
1563
|
+
// data-updated fires separately and triggers refreshData()
|
|
1564
|
+
});
|
|
1565
|
+
|
|
1566
|
+
es.addEventListener('coverage-error', () => {
|
|
1567
|
+
coverageRunning = false;
|
|
1568
|
+
coverageHasError = true;
|
|
1569
|
+
updateCovBtn();
|
|
1570
|
+
setTimeout(() => { coverageHasError = false; updateCovBtn(); }, 8000);
|
|
1571
|
+
});
|
|
1572
|
+
|
|
1573
|
+
es.addEventListener('agent-started', (e) => {
|
|
1574
|
+
agentRunning = true;
|
|
1575
|
+
const { title } = JSON.parse(e.data);
|
|
1576
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
1577
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1578
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
1579
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
1580
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
1581
|
+
});
|
|
1582
|
+
|
|
1583
|
+
es.addEventListener('agent-output', (e) => {
|
|
1584
|
+
const { line, isError } = JSON.parse(e.data);
|
|
1585
|
+
appendTerminalLine(line, !!isError);
|
|
1586
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
1587
|
+
});
|
|
1588
|
+
|
|
1589
|
+
es.addEventListener('agent-done', () => {
|
|
1590
|
+
agentRunning = false;
|
|
1591
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
1592
|
+
renderContent();
|
|
1593
|
+
});
|
|
1594
|
+
|
|
1595
|
+
es.addEventListener('agent-summary', (e) => {
|
|
1596
|
+
const { passed, failed, files } = JSON.parse(e.data);
|
|
1597
|
+
const term = document.getElementById('agentTerminal');
|
|
1598
|
+
const allOk = failed === 0;
|
|
1599
|
+
const box = document.createElement('div');
|
|
1600
|
+
box.style.cssText = `
|
|
1601
|
+
margin: 10px 0 4px;
|
|
1602
|
+
padding: 10px 14px;
|
|
1603
|
+
border-radius: 8px;
|
|
1604
|
+
border: 1px solid ${allOk ? 'var(--green)' : 'var(--red)'};
|
|
1605
|
+
background: ${allOk ? '#0d2a1a' : '#2a0d0d'};
|
|
1606
|
+
font-family: inherit;
|
|
1607
|
+
`;
|
|
1608
|
+
box.innerHTML = `
|
|
1609
|
+
<div style="font-size:13px;font-weight:700;color:${allOk ? 'var(--green)' : 'var(--red)'}">
|
|
1610
|
+
${allOk ? '✅' : '⚠️'} Тесты: ${passed} passed${failed > 0 ? ', ' + failed + ' failed' : ''}
|
|
1611
|
+
</div>
|
|
1612
|
+
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
1613
|
+
`;
|
|
1614
|
+
term.appendChild(box);
|
|
1615
|
+
term.scrollTop = term.scrollHeight;
|
|
1616
|
+
});
|
|
1617
|
+
|
|
1618
|
+
es.addEventListener('agent-error', (e) => {
|
|
1619
|
+
agentRunning = false;
|
|
1620
|
+
const { message } = JSON.parse(e.data);
|
|
1621
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
1622
|
+
appendTerminalLine('❌ ' + (message || 'Ошибка агента'), true);
|
|
1623
|
+
});
|
|
1624
|
+
|
|
1045
1625
|
es.onerror = () => {
|
|
1046
1626
|
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
1047
1627
|
es.close();
|