viberadar 0.3.3 → 0.3.5
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 +10 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +78 -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 +738 -23
- 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;
|
|
@@ -339,6 +376,187 @@
|
|
|
339
376
|
font-family: monospace;
|
|
340
377
|
}
|
|
341
378
|
|
|
379
|
+
/* ── Feature drill-down ──────────────────────────────────────────────────── */
|
|
380
|
+
.drill-header {
|
|
381
|
+
display: flex;
|
|
382
|
+
align-items: center;
|
|
383
|
+
gap: 14px;
|
|
384
|
+
padding-bottom: 14px;
|
|
385
|
+
border-bottom: 1px solid var(--border);
|
|
386
|
+
margin-bottom: 16px;
|
|
387
|
+
flex-wrap: wrap;
|
|
388
|
+
}
|
|
389
|
+
.back-btn {
|
|
390
|
+
background: none;
|
|
391
|
+
border: 1px solid var(--border);
|
|
392
|
+
border-radius: 6px;
|
|
393
|
+
color: var(--muted);
|
|
394
|
+
cursor: pointer;
|
|
395
|
+
padding: 5px 12px;
|
|
396
|
+
font-size: 12px;
|
|
397
|
+
flex-shrink: 0;
|
|
398
|
+
transition: background 0.1s, color 0.1s;
|
|
399
|
+
}
|
|
400
|
+
.back-btn:hover { background: var(--border); color: var(--text); }
|
|
401
|
+
.drill-title {
|
|
402
|
+
display: flex; align-items: center; gap: 8px;
|
|
403
|
+
font-size: 17px; font-weight: 700;
|
|
404
|
+
}
|
|
405
|
+
.drill-stats {
|
|
406
|
+
display: flex; gap: 16px;
|
|
407
|
+
font-size: 12px; color: var(--muted);
|
|
408
|
+
margin-left: auto;
|
|
409
|
+
}
|
|
410
|
+
.drill-desc {
|
|
411
|
+
font-size: 13px; color: var(--muted);
|
|
412
|
+
margin-bottom: 14px; line-height: 1.5;
|
|
413
|
+
}
|
|
414
|
+
.drill-section-label {
|
|
415
|
+
font-size: 10px; text-transform: uppercase;
|
|
416
|
+
letter-spacing: 0.5px; color: var(--muted);
|
|
417
|
+
margin: 14px 0 6px;
|
|
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
|
+
}
|
|
458
|
+
.file-rows { display: flex; flex-direction: column; gap: 2px; }
|
|
459
|
+
.file-row {
|
|
460
|
+
display: grid;
|
|
461
|
+
grid-template-columns: 22px 1fr auto;
|
|
462
|
+
align-items: center;
|
|
463
|
+
gap: 8px;
|
|
464
|
+
padding: 7px 10px;
|
|
465
|
+
border-radius: 6px;
|
|
466
|
+
cursor: pointer;
|
|
467
|
+
font-size: 13px;
|
|
468
|
+
transition: background 0.1s;
|
|
469
|
+
}
|
|
470
|
+
.file-row:hover { background: var(--bg-card); }
|
|
471
|
+
.file-row.active { background: var(--bg-hover); border-left: 2px solid var(--blue); padding-left: 8px; }
|
|
472
|
+
.file-row-icon { font-size: 12px; }
|
|
473
|
+
.file-row-name { font-weight: 500; word-break: break-all; }
|
|
474
|
+
.file-row-dir { font-size: 11px; color: var(--dim); text-align: right; word-break: break-word; }
|
|
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
|
+
|
|
342
560
|
/* ── Misc ────────────────────────────────────────────────────────────────── */
|
|
343
561
|
.loading { display: flex; align-items: center; justify-content: center; height: 200px; color: var(--muted); font-size: 14px; }
|
|
344
562
|
.empty { text-align: center; padding: 40px 20px; color: var(--muted); font-size: 14px; }
|
|
@@ -351,6 +569,8 @@
|
|
|
351
569
|
<h1>VibeRadar</h1>
|
|
352
570
|
<span class="header-project" id="projectName">—</span>
|
|
353
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>
|
|
354
574
|
<span id="liveDot" title="Connecting…" style="
|
|
355
575
|
width:8px; height:8px; border-radius:50%;
|
|
356
576
|
background:var(--dim); display:inline-block;
|
|
@@ -381,6 +601,15 @@
|
|
|
381
601
|
<div id="panelContent"></div>
|
|
382
602
|
</div>
|
|
383
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
|
+
|
|
384
613
|
<script>
|
|
385
614
|
// ─── State ────────────────────────────────────────────────────────────────────
|
|
386
615
|
let D = null;
|
|
@@ -388,6 +617,79 @@ let view = 'features';
|
|
|
388
617
|
let searchQuery = '';
|
|
389
618
|
let activeTypes = new Set();
|
|
390
619
|
let activePanelKey = 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
|
+
}
|
|
391
693
|
|
|
392
694
|
// ─── Color helpers ────────────────────────────────────────────────────────────
|
|
393
695
|
const TYPE_COLORS = {
|
|
@@ -421,9 +723,19 @@ function pluralFiles(n) {
|
|
|
421
723
|
// ─── Init ─────────────────────────────────────────────────────────────────────
|
|
422
724
|
async function init() {
|
|
423
725
|
try {
|
|
424
|
-
const res = await
|
|
726
|
+
const [res, statusRes] = await Promise.all([
|
|
727
|
+
fetch('/api/data'),
|
|
728
|
+
fetch('/api/status').catch(() => null),
|
|
729
|
+
]);
|
|
425
730
|
D = await res.json();
|
|
426
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
|
+
|
|
427
739
|
document.getElementById('projectName').textContent = D.projectName;
|
|
428
740
|
document.getElementById('scannedAt').textContent =
|
|
429
741
|
new Date(D.scannedAt).toLocaleTimeString();
|
|
@@ -452,7 +764,7 @@ function renderStats() {
|
|
|
452
764
|
|
|
453
765
|
let items;
|
|
454
766
|
if (D.hasConfig && D.features) {
|
|
455
|
-
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;
|
|
456
768
|
items = [
|
|
457
769
|
{ v: D.features.length, l: 'Features' },
|
|
458
770
|
{ v: src.length, l: 'Source Files' },
|
|
@@ -519,7 +831,28 @@ function renderSidebar() {
|
|
|
519
831
|
// ─── Content ──────────────────────────────────────────────────────────────────
|
|
520
832
|
function renderContent() {
|
|
521
833
|
const c = document.getElementById('content');
|
|
522
|
-
view === 'features'
|
|
834
|
+
if (view === 'features') {
|
|
835
|
+
if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
|
|
836
|
+
else if (drillFeatureKey) renderFeatureDetail(c);
|
|
837
|
+
else renderFeatureCards(c);
|
|
838
|
+
} else {
|
|
839
|
+
renderModuleGrid(c);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function backToFeatureDetail() {
|
|
844
|
+
drillTestType = null;
|
|
845
|
+
activePanelKey = null;
|
|
846
|
+
document.getElementById('panel').classList.remove('open');
|
|
847
|
+
renderContent();
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
function backToFeatures() {
|
|
851
|
+
drillFeatureKey = null;
|
|
852
|
+
drillTestType = null;
|
|
853
|
+
activePanelKey = null;
|
|
854
|
+
document.getElementById('panel').classList.remove('open');
|
|
855
|
+
renderContent();
|
|
523
856
|
}
|
|
524
857
|
|
|
525
858
|
function renderFeatureCards(c) {
|
|
@@ -542,12 +875,25 @@ function renderFeatureCards(c) {
|
|
|
542
875
|
|
|
543
876
|
if (!list.length) { c.innerHTML = '<div class="empty">Ничего не найдено</div>'; return; }
|
|
544
877
|
|
|
545
|
-
|
|
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>';
|
|
546
890
|
const grid = document.getElementById('featGrid');
|
|
547
891
|
|
|
548
892
|
list.forEach(f => {
|
|
549
893
|
const pct = f.fileCount > 0 ? Math.round(f.testedCount / f.fileCount * 100) : 0;
|
|
550
894
|
const isActive = activePanelKey === f.key;
|
|
895
|
+
const hasCov = f.coveragePct != null;
|
|
896
|
+
const covPct = hasCov ? Math.round(f.coveragePct) : null;
|
|
551
897
|
|
|
552
898
|
const card = document.createElement('div');
|
|
553
899
|
card.className = 'feature-card' + (isActive ? ' active' : '');
|
|
@@ -559,21 +905,51 @@ function renderFeatureCards(c) {
|
|
|
559
905
|
<span class="feature-file-count">${f.fileCount} ${pluralFiles(f.fileCount)}</span>
|
|
560
906
|
</div>
|
|
561
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>`}
|
|
562
921
|
<div class="feature-progress-wrap">
|
|
563
922
|
<div class="feature-progress-bar">
|
|
564
923
|
<div class="feature-progress-fill" style="width:${pct}%;background:${f.color}"></div>
|
|
565
924
|
</div>
|
|
566
|
-
<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>
|
|
567
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>` : ''}
|
|
568
931
|
</div>`;
|
|
569
|
-
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
|
+
}
|
|
570
945
|
grid.appendChild(card);
|
|
571
946
|
});
|
|
572
947
|
|
|
573
948
|
// ── Unmapped card ──────────────────────────────────────────────────────────
|
|
574
949
|
if (!q) {
|
|
950
|
+
const infraSrc = D.modules.filter(m => m.type !== 'test' && m.isInfra);
|
|
575
951
|
const unmappedSrc = D.modules.filter(m =>
|
|
576
|
-
m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
|
|
952
|
+
m.type !== 'test' && !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)
|
|
577
953
|
);
|
|
578
954
|
if (unmappedSrc.length > 0) {
|
|
579
955
|
const isActive = activePanelKey === '__unmapped__';
|
|
@@ -581,6 +957,9 @@ function renderFeatureCards(c) {
|
|
|
581
957
|
card.className = 'feature-card' + (isActive ? ' active' : '');
|
|
582
958
|
card.style.borderStyle = 'dashed';
|
|
583
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
|
+
: '';
|
|
584
963
|
card.innerHTML = `
|
|
585
964
|
<div class="feature-accent" style="background:var(--yellow)"></div>
|
|
586
965
|
<div class="feature-body">
|
|
@@ -588,7 +967,7 @@ function renderFeatureCards(c) {
|
|
|
588
967
|
<span style="color:var(--yellow)">⚠ Unmapped</span>
|
|
589
968
|
<span class="feature-file-count">${unmappedSrc.length} ${pluralFiles(unmappedSrc.length)}</span>
|
|
590
969
|
</div>
|
|
591
|
-
<div class="feature-desc">Файлы вне карты фич — не входят ни в одну
|
|
970
|
+
<div class="feature-desc">Файлы вне карты фич — не входят ни в одну фичу${infraNote}</div>
|
|
592
971
|
<div class="feature-progress-wrap">
|
|
593
972
|
<div class="feature-progress-bar">
|
|
594
973
|
<div class="feature-progress-fill" style="width:100%;background:var(--border)"></div>
|
|
@@ -596,12 +975,261 @@ function renderFeatureCards(c) {
|
|
|
596
975
|
<span class="feature-progress-label" style="color:var(--dim)">нет привязки</span>
|
|
597
976
|
</div>
|
|
598
977
|
</div>`;
|
|
599
|
-
card.onclick = () =>
|
|
978
|
+
card.onclick = () => {
|
|
979
|
+
drillFeatureKey = '__unmapped__';
|
|
980
|
+
activePanelKey = null;
|
|
981
|
+
document.getElementById('panel').classList.remove('open');
|
|
982
|
+
renderContent();
|
|
983
|
+
};
|
|
600
984
|
grid.appendChild(card);
|
|
601
985
|
}
|
|
602
986
|
}
|
|
603
987
|
}
|
|
604
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
|
+
|
|
1002
|
+
function renderFeatureDetail(c) {
|
|
1003
|
+
const feat = D.features.find(f => f.key === drillFeatureKey);
|
|
1004
|
+
if (!feat) { backToFeatures(); return; }
|
|
1005
|
+
|
|
1006
|
+
const mods = D.modules.filter(m => m.featureKeys && m.featureKeys.includes(drillFeatureKey));
|
|
1007
|
+
const src = mods.filter(m => m.type !== 'test');
|
|
1008
|
+
const tst = mods.filter(m => m.type === 'test');
|
|
1009
|
+
const testedCount = src.filter(m => m.hasTests).length;
|
|
1010
|
+
const pct = src.length > 0 ? Math.round(testedCount / src.length * 100) : 0;
|
|
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
|
+
|
|
1026
|
+
const q = searchQuery.toLowerCase();
|
|
1027
|
+
const filtered = q ? listFiles.filter(m =>
|
|
1028
|
+
m.name.toLowerCase().includes(q) || m.relativePath.toLowerCase().includes(q)
|
|
1029
|
+
) : listFiles;
|
|
1030
|
+
|
|
1031
|
+
c.innerHTML = `
|
|
1032
|
+
<div class="drill-header">
|
|
1033
|
+
<button class="back-btn" onclick="backToFeatures()">← Все фичи</button>
|
|
1034
|
+
<div class="drill-title">
|
|
1035
|
+
<div style="width:10px;height:10px;border-radius:50%;background:${feat.color};flex-shrink:0"></div>
|
|
1036
|
+
<span>${feat.label}</span>
|
|
1037
|
+
</div>
|
|
1038
|
+
<div class="drill-stats">
|
|
1039
|
+
<span>${src.length} файлов</span>
|
|
1040
|
+
<span style="color:${covColor(pct)}">${pct}% с тестами</span>
|
|
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
|
+
}
|
|
1045
|
+
</div>
|
|
1046
|
+
</div>
|
|
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>
|
|
1188
|
+
<div class="file-rows" id="fileRows">
|
|
1189
|
+
${filtered.length === 0
|
|
1190
|
+
? '<div style="font-size:13px;color:var(--dim)">Ничего не найдено</div>'
|
|
1191
|
+
: filtered.map(m => fileRow(m)).join('')
|
|
1192
|
+
}
|
|
1193
|
+
</div>`;
|
|
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
|
+
|
|
1211
|
+
c.querySelectorAll('.file-row[data-id]').forEach(row => {
|
|
1212
|
+
row.onclick = () => {
|
|
1213
|
+
const m = D.modules.find(m => m.id === row.dataset.id);
|
|
1214
|
+
if (m) openModulePanel(m);
|
|
1215
|
+
};
|
|
1216
|
+
});
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
function fileRow(m, isTest = false) {
|
|
1220
|
+
const parts = m.relativePath.replace(/\\/g, '/').split('/');
|
|
1221
|
+
const name = parts[parts.length - 1];
|
|
1222
|
+
const dir = parts.slice(0, -1).join('/');
|
|
1223
|
+
const icon = isTest ? '🧪' : (m.hasTests ? '✅' : '⬜');
|
|
1224
|
+
const isActive = activePanelKey === m.id;
|
|
1225
|
+
return `
|
|
1226
|
+
<div class="file-row${isActive ? ' active' : ''}" data-id="${m.id}">
|
|
1227
|
+
<span class="file-row-icon">${icon}</span>
|
|
1228
|
+
<span class="file-row-name">${name}</span>
|
|
1229
|
+
<span class="file-row-dir">${dir}</span>
|
|
1230
|
+
</div>`;
|
|
1231
|
+
}
|
|
1232
|
+
|
|
605
1233
|
function renderModuleGrid(c) {
|
|
606
1234
|
const q = searchQuery.toLowerCase();
|
|
607
1235
|
const list = D.modules.filter(m => {
|
|
@@ -757,7 +1385,8 @@ function openModulePanel(m) {
|
|
|
757
1385
|
document.getElementById('panel').classList.add('open');
|
|
758
1386
|
}
|
|
759
1387
|
|
|
760
|
-
function openUnmappedPanel(files) {
|
|
1388
|
+
function openUnmappedPanel(files, infraFiles) {
|
|
1389
|
+
infraFiles = infraFiles || [];
|
|
761
1390
|
activePanelKey = '__unmapped__';
|
|
762
1391
|
renderContent();
|
|
763
1392
|
|
|
@@ -771,6 +1400,9 @@ function openUnmappedPanel(files) {
|
|
|
771
1400
|
});
|
|
772
1401
|
const dirs = Object.keys(byDir).sort();
|
|
773
1402
|
|
|
1403
|
+
// Build feature list for context
|
|
1404
|
+
const featureList = (D.features || []).map(f => ` • ${f.key} — ${f.label}`).join('\n');
|
|
1405
|
+
|
|
774
1406
|
// Build plain-text list for copying to AI agent
|
|
775
1407
|
const plainList = files
|
|
776
1408
|
.map(m => '- ' + m.relativePath.replace(/\\/g, '/'))
|
|
@@ -778,26 +1410,38 @@ function openUnmappedPanel(files) {
|
|
|
778
1410
|
|
|
779
1411
|
const promptText =
|
|
780
1412
|
`В проекте ${files.length} файлов без привязки к фичам (unmapped).\n` +
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1413
|
+
`\nДля каждого файла из списка реши:\n` +
|
|
1414
|
+
`1. Если файл относится к конкретной фиче → добавь его путь в "include" этой фичи в viberadar.config.json\n` +
|
|
1415
|
+
`2. Если это инфраструктура (утилиты, конфиги, middleware, типы, бутстрап) → добавь glob в массив "ignore"\n` +
|
|
1416
|
+
`3. Если файл явно бизнес-логика новой фичи → создай новую фичу\n` +
|
|
1417
|
+
`4. Если непонятно — пропусти\n` +
|
|
1418
|
+
`\nСуществующие фичи:\n${featureList}\n` +
|
|
1419
|
+
`\nФайлы:\n${plainList}`;
|
|
1420
|
+
|
|
1421
|
+
const infraNote = infraFiles.length > 0
|
|
1422
|
+
? `<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)">
|
|
1423
|
+
🔒 <b style="color:var(--text)">${infraFiles.length} infra/system файлов</b> скрыты (добавлены в <code>ignore</code>)<br>
|
|
1424
|
+
Они не считаются unmapped и не показываются в карте фич.
|
|
1425
|
+
</div>`
|
|
1426
|
+
: '';
|
|
784
1427
|
|
|
785
1428
|
document.getElementById('panelContent').innerHTML = `
|
|
786
1429
|
<div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
|
|
787
1430
|
<span style="font-size:16px">⚠</span>
|
|
788
1431
|
<div class="panel-title" style="color:var(--yellow)">Unmapped файлы</div>
|
|
789
1432
|
</div>
|
|
790
|
-
<div class="panel-subtitle">
|
|
791
|
-
Не входят ни в одну
|
|
792
|
-
Запусти <code style="color:var(--blue)">npx viberadar init</code> — агент предложит куда добавить.
|
|
1433
|
+
<div class="panel-subtitle" style="margin-bottom:12px">
|
|
1434
|
+
Не входят ни в одну фичу. Скопируй список и отправь AI-агенту.
|
|
793
1435
|
</div>
|
|
794
1436
|
|
|
1437
|
+
${infraNote}
|
|
1438
|
+
|
|
795
1439
|
<button id="copyUnmapped" style="
|
|
796
1440
|
width:100%; padding:8px 12px; margin-bottom:16px;
|
|
797
1441
|
background:var(--bg); border:1px solid var(--border);
|
|
798
1442
|
border-radius:6px; color:var(--blue); font-size:12px;
|
|
799
1443
|
cursor:pointer; text-align:left;
|
|
800
|
-
">📋 Скопировать
|
|
1444
|
+
">📋 Скопировать промпт для AI-агента (${files.length} файлов)</button>
|
|
801
1445
|
|
|
802
1446
|
${dirs.map(dir => `
|
|
803
1447
|
<div class="panel-section">
|
|
@@ -833,6 +1477,8 @@ document.querySelectorAll('.view-tab').forEach(tab => {
|
|
|
833
1477
|
tab.onclick = () => {
|
|
834
1478
|
if (tab.classList.contains('disabled')) return;
|
|
835
1479
|
view = tab.dataset.view;
|
|
1480
|
+
drillFeatureKey = null;
|
|
1481
|
+
drillTestType = null;
|
|
836
1482
|
activePanelKey = null;
|
|
837
1483
|
searchQuery = '';
|
|
838
1484
|
activeTypes.clear();
|
|
@@ -871,14 +1517,11 @@ async function refreshData() {
|
|
|
871
1517
|
renderSidebar();
|
|
872
1518
|
renderContent();
|
|
873
1519
|
|
|
874
|
-
// Re-
|
|
1520
|
+
// Re-render drill-down or re-open panel
|
|
875
1521
|
const panelOpen = document.getElementById('panel').classList.contains('open');
|
|
876
1522
|
if (panelOpen && activePanelKey) {
|
|
877
|
-
if (
|
|
878
|
-
|
|
879
|
-
m.type !== 'test' && (!m.featureKeys || m.featureKeys.length === 0)
|
|
880
|
-
);
|
|
881
|
-
openUnmappedPanel(unmapped);
|
|
1523
|
+
if (drillFeatureKey === '__unmapped__') {
|
|
1524
|
+
renderContent(); // already routes to renderUnmappedDetail
|
|
882
1525
|
} else if (view === 'features' && D.features) {
|
|
883
1526
|
openFeaturePanel(activePanelKey);
|
|
884
1527
|
} else {
|
|
@@ -906,6 +1549,78 @@ function connectSSE() {
|
|
|
906
1549
|
refreshData();
|
|
907
1550
|
});
|
|
908
1551
|
|
|
1552
|
+
es.addEventListener('coverage-started', () => {
|
|
1553
|
+
coverageRunning = true;
|
|
1554
|
+
coverageHasError = false;
|
|
1555
|
+
updateCovBtn();
|
|
1556
|
+
});
|
|
1557
|
+
|
|
1558
|
+
es.addEventListener('coverage-done', () => {
|
|
1559
|
+
coverageRunning = false;
|
|
1560
|
+
coverageHasError = false;
|
|
1561
|
+
updateCovBtn();
|
|
1562
|
+
// data-updated fires separately and triggers refreshData()
|
|
1563
|
+
});
|
|
1564
|
+
|
|
1565
|
+
es.addEventListener('coverage-error', () => {
|
|
1566
|
+
coverageRunning = false;
|
|
1567
|
+
coverageHasError = true;
|
|
1568
|
+
updateCovBtn();
|
|
1569
|
+
setTimeout(() => { coverageHasError = false; updateCovBtn(); }, 8000);
|
|
1570
|
+
});
|
|
1571
|
+
|
|
1572
|
+
es.addEventListener('agent-started', (e) => {
|
|
1573
|
+
agentRunning = true;
|
|
1574
|
+
const { title } = JSON.parse(e.data);
|
|
1575
|
+
document.getElementById('agentPanelTitle').textContent = '🤖 ' + title;
|
|
1576
|
+
document.getElementById('agentPanelStatus').textContent = 'запускаю…';
|
|
1577
|
+
document.getElementById('agentPanel').classList.add('open');
|
|
1578
|
+
document.getElementById('termBtn').classList.add('term-active');
|
|
1579
|
+
document.getElementById('agentTerminal').innerHTML = '';
|
|
1580
|
+
});
|
|
1581
|
+
|
|
1582
|
+
es.addEventListener('agent-output', (e) => {
|
|
1583
|
+
const { line, isError } = JSON.parse(e.data);
|
|
1584
|
+
appendTerminalLine(line, !!isError);
|
|
1585
|
+
document.getElementById('agentPanelStatus').textContent = 'работает…';
|
|
1586
|
+
});
|
|
1587
|
+
|
|
1588
|
+
es.addEventListener('agent-done', () => {
|
|
1589
|
+
agentRunning = false;
|
|
1590
|
+
document.getElementById('agentPanelStatus').textContent = '✅ готово';
|
|
1591
|
+
renderContent();
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
es.addEventListener('agent-summary', (e) => {
|
|
1595
|
+
const { passed, failed, files } = JSON.parse(e.data);
|
|
1596
|
+
const term = document.getElementById('agentTerminal');
|
|
1597
|
+
const allOk = failed === 0;
|
|
1598
|
+
const box = document.createElement('div');
|
|
1599
|
+
box.style.cssText = `
|
|
1600
|
+
margin: 10px 0 4px;
|
|
1601
|
+
padding: 10px 14px;
|
|
1602
|
+
border-radius: 8px;
|
|
1603
|
+
border: 1px solid ${allOk ? 'var(--green)' : 'var(--red)'};
|
|
1604
|
+
background: ${allOk ? '#0d2a1a' : '#2a0d0d'};
|
|
1605
|
+
font-family: inherit;
|
|
1606
|
+
`;
|
|
1607
|
+
box.innerHTML = `
|
|
1608
|
+
<div style="font-size:13px;font-weight:700;color:${allOk ? 'var(--green)' : 'var(--red)'}">
|
|
1609
|
+
${allOk ? '✅' : '⚠️'} Тесты: ${passed} passed${failed > 0 ? ', ' + failed + ' failed' : ''}
|
|
1610
|
+
</div>
|
|
1611
|
+
${files.map(f => `<div style="font-size:11px;color:var(--dim);margin-top:3px">📄 ${f}</div>`).join('')}
|
|
1612
|
+
`;
|
|
1613
|
+
term.appendChild(box);
|
|
1614
|
+
term.scrollTop = term.scrollHeight;
|
|
1615
|
+
});
|
|
1616
|
+
|
|
1617
|
+
es.addEventListener('agent-error', (e) => {
|
|
1618
|
+
agentRunning = false;
|
|
1619
|
+
const { message } = JSON.parse(e.data);
|
|
1620
|
+
document.getElementById('agentPanelStatus').textContent = '❌ ошибка';
|
|
1621
|
+
appendTerminalLine('❌ ' + (message || 'Ошибка агента'), true);
|
|
1622
|
+
});
|
|
1623
|
+
|
|
909
1624
|
es.onerror = () => {
|
|
910
1625
|
setLiveDot('var(--dim)', 'Нет соединения — переподключаюсь…');
|
|
911
1626
|
es.close();
|