viberadar 0.3.113 → 0.3.115
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/init/prompt-builder.d.ts +3 -0
- package/dist/init/prompt-builder.d.ts.map +1 -1
- package/dist/init/prompt-builder.js +144 -17
- package/dist/init/prompt-builder.js.map +1 -1
- package/dist/scanner/index.d.ts +71 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +298 -0
- package/dist/scanner/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +213 -1
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +499 -1
- package/package.json +7 -5
package/dist/ui/dashboard.html
CHANGED
|
@@ -1354,6 +1354,72 @@
|
|
|
1354
1354
|
.e2e-screenshot-modal { position:fixed; inset:0; background:rgba(0,0,0,0.8); z-index:9999; display:flex; align-items:center; justify-content:center; cursor:pointer; }
|
|
1355
1355
|
.e2e-screenshot-modal img { max-width:90vw; max-height:90vh; border-radius:8px; }
|
|
1356
1356
|
.e2e-plan-indicator { font-size:10px; color:#d2a8ff; margin-left:4px; }
|
|
1357
|
+
|
|
1358
|
+
/* ─── Service Map styles ──────────────────────────────────────────────────── */
|
|
1359
|
+
.svc-sidebar-tabs { display:flex; flex-direction:column; gap:4px; padding:0 6px; }
|
|
1360
|
+
.svc-sidebar-tab { background:none; border:1px solid var(--border); color:var(--text); padding:6px 10px; border-radius:6px; cursor:pointer; font-size:12px; text-align:left; transition:all 0.15s; }
|
|
1361
|
+
.svc-sidebar-tab:hover { background:var(--surface); }
|
|
1362
|
+
.svc-sidebar-tab.active { background:var(--accent); color:#fff; border-color:var(--accent); }
|
|
1363
|
+
|
|
1364
|
+
.svc-graph-wrap { position:relative; width:100%; height:calc(100vh - 160px); overflow:hidden; border:1px solid var(--border); border-radius:8px; background:var(--bg); }
|
|
1365
|
+
.svc-graph-svg { width:100%; height:100%; }
|
|
1366
|
+
.svc-graph-svg text { font-family:inherit; }
|
|
1367
|
+
|
|
1368
|
+
.svc-node-group { cursor:pointer; }
|
|
1369
|
+
.svc-node-rect { rx:8; ry:8; stroke-width:1.5; transition:stroke 0.15s, filter 0.15s; }
|
|
1370
|
+
.svc-node-group:hover .svc-node-rect { stroke:var(--accent); filter:drop-shadow(0 0 6px rgba(88,166,255,0.4)); }
|
|
1371
|
+
.svc-node-icon { font-size:18px; pointer-events:none; }
|
|
1372
|
+
.svc-node-label { fill:var(--text); font-size:11px; pointer-events:none; }
|
|
1373
|
+
.svc-node-category { fill:var(--muted); font-size:9px; pointer-events:none; }
|
|
1374
|
+
|
|
1375
|
+
.svc-edge { fill:none; stroke:var(--border); stroke-width:1.5; }
|
|
1376
|
+
.svc-edge.critical { stroke:#f85149; stroke-width:2.5; }
|
|
1377
|
+
.svc-edge-label { fill:var(--muted); font-size:9px; }
|
|
1378
|
+
.svc-edge.highlighted { stroke:var(--accent); stroke-width:2; }
|
|
1379
|
+
|
|
1380
|
+
.svc-group-label { fill:var(--muted); font-size:10px; text-transform:uppercase; letter-spacing:1px; }
|
|
1381
|
+
|
|
1382
|
+
.svc-legend { position:absolute; bottom:12px; left:12px; display:flex; gap:12px; font-size:11px; color:var(--muted); background:var(--surface); padding:6px 12px; border-radius:6px; border:1px solid var(--border); }
|
|
1383
|
+
.svc-legend-item { display:flex; align-items:center; gap:4px; }
|
|
1384
|
+
.svc-legend-dot { width:10px; height:10px; border-radius:50%; }
|
|
1385
|
+
|
|
1386
|
+
.svc-pipeline-container { display:flex; flex-direction:column; gap:24px; padding:16px; }
|
|
1387
|
+
.svc-pipeline { background:var(--surface); border:1px solid var(--border); border-radius:8px; padding:16px; }
|
|
1388
|
+
.svc-pipeline-title { font-size:14px; font-weight:600; color:var(--text); margin-bottom:4px; }
|
|
1389
|
+
.svc-pipeline-desc { font-size:12px; color:var(--muted); margin-bottom:12px; }
|
|
1390
|
+
.svc-pipeline-triggers { display:flex; gap:6px; flex-wrap:wrap; margin-bottom:12px; }
|
|
1391
|
+
.svc-pipeline-trigger { font-size:10px; padding:2px 8px; border-radius:10px; background:rgba(88,166,255,0.15); color:#58a6ff; }
|
|
1392
|
+
.svc-pipeline-steps { display:flex; align-items:center; gap:0; overflow-x:auto; padding:8px 0; }
|
|
1393
|
+
.svc-step { display:flex; flex-direction:column; align-items:center; min-width:100px; flex-shrink:0; }
|
|
1394
|
+
.svc-step-box { background:var(--bg); border:1px solid var(--border); border-radius:6px; padding:8px 12px; text-align:center; width:100px; cursor:pointer; transition:border-color 0.15s; }
|
|
1395
|
+
.svc-step-box:hover { border-color:var(--accent); }
|
|
1396
|
+
.svc-step-label { font-size:11px; color:var(--text); font-weight:500; }
|
|
1397
|
+
.svc-step-service { font-size:9px; color:var(--muted); margin-top:2px; }
|
|
1398
|
+
.svc-step-arrow { color:var(--muted); font-size:16px; padding:0 4px; flex-shrink:0; }
|
|
1399
|
+
|
|
1400
|
+
.svc-export { padding:16px; }
|
|
1401
|
+
.svc-export-section { margin-bottom:24px; }
|
|
1402
|
+
.svc-export-title { font-size:14px; font-weight:600; color:var(--text); margin-bottom:8px; }
|
|
1403
|
+
.svc-export-table { width:100%; border-collapse:collapse; font-size:12px; }
|
|
1404
|
+
.svc-export-table th { text-align:left; padding:6px 8px; border-bottom:1px solid var(--border); color:var(--muted); font-weight:500; }
|
|
1405
|
+
.svc-export-table td { padding:6px 8px; border-bottom:1px solid rgba(255,255,255,0.05); color:var(--text); }
|
|
1406
|
+
.svc-export-btns { display:flex; gap:8px; margin-top:16px; flex-wrap:wrap; }
|
|
1407
|
+
.svc-export-btn { background:var(--surface); border:1px solid var(--border); color:var(--text); padding:8px 16px; border-radius:6px; cursor:pointer; font-size:12px; transition:all 0.15s; }
|
|
1408
|
+
.svc-export-btn:hover { background:var(--accent); color:#fff; border-color:var(--accent); }
|
|
1409
|
+
.svc-alert-badge { display:inline-block; padding:1px 6px; border-radius:8px; font-size:10px; font-weight:500; }
|
|
1410
|
+
.svc-alert-badge.critical { background:rgba(248,81,73,0.2); color:#f85149; }
|
|
1411
|
+
.svc-alert-badge.warning { background:rgba(227,179,65,0.2); color:#e3b341; }
|
|
1412
|
+
.svc-alert-badge.info { background:rgba(88,166,255,0.2); color:#58a6ff; }
|
|
1413
|
+
|
|
1414
|
+
.svc-empty { text-align:center; padding:60px 20px; color:var(--muted); }
|
|
1415
|
+
.svc-empty h3 { color:var(--text); margin-bottom:8px; }
|
|
1416
|
+
|
|
1417
|
+
/* Service detail panel */
|
|
1418
|
+
.svc-detail-section { margin-bottom:16px; }
|
|
1419
|
+
.svc-detail-section h4 { font-size:12px; color:var(--muted); margin-bottom:6px; text-transform:uppercase; letter-spacing:0.5px; }
|
|
1420
|
+
.svc-detail-row { font-size:12px; color:var(--text); padding:3px 0; }
|
|
1421
|
+
.svc-detail-badge { display:inline-block; padding:2px 8px; border-radius:4px; font-size:11px; }
|
|
1422
|
+
|
|
1357
1423
|
</style>
|
|
1358
1424
|
</head>
|
|
1359
1425
|
<body>
|
|
@@ -1501,17 +1567,20 @@ const modeStore = {
|
|
|
1501
1567
|
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
|
|
1502
1568
|
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
|
|
1503
1569
|
docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
|
|
1570
|
+
services: { view: 'graph', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null, svcTab: 'graph' },
|
|
1504
1571
|
};
|
|
1505
1572
|
|
|
1506
1573
|
function getModeFromPath(pathname = window.location.pathname) {
|
|
1507
1574
|
if (pathname.startsWith('/radar/observability')) return 'observability';
|
|
1508
1575
|
if (pathname.startsWith('/radar/docs')) return 'docs';
|
|
1576
|
+
if (pathname.startsWith('/radar/services')) return 'services';
|
|
1509
1577
|
return 'qa';
|
|
1510
1578
|
}
|
|
1511
1579
|
|
|
1512
1580
|
function routePathForMode(mode) {
|
|
1513
1581
|
if (mode === 'observability') return '/radar/observability';
|
|
1514
1582
|
if (mode === 'docs') return '/radar/docs';
|
|
1583
|
+
if (mode === 'services') return '/radar/services';
|
|
1515
1584
|
return '/radar/qa';
|
|
1516
1585
|
}
|
|
1517
1586
|
|
|
@@ -1555,7 +1624,7 @@ function switchMode(nextMode) {
|
|
|
1555
1624
|
saveModeState(contextMode);
|
|
1556
1625
|
contextMode = nextMode;
|
|
1557
1626
|
restoreModeState(contextMode);
|
|
1558
|
-
if (contextMode === 'observability' || contextMode === 'docs') {
|
|
1627
|
+
if (contextMode === 'observability' || contextMode === 'docs' || contextMode === 'services') {
|
|
1559
1628
|
view = 'features';
|
|
1560
1629
|
drillFeatureKey = null;
|
|
1561
1630
|
drillTestType = null;
|
|
@@ -2795,6 +2864,25 @@ function renderStats() {
|
|
|
2795
2864
|
{ v: '—', l: 'Шумных паттернов' },
|
|
2796
2865
|
];
|
|
2797
2866
|
}
|
|
2867
|
+
} else if (contextMode === 'services') {
|
|
2868
|
+
const sm = D.serviceMap;
|
|
2869
|
+
if (sm) {
|
|
2870
|
+
items = [
|
|
2871
|
+
{ v: sm.nodes.length, l: 'Сервисов' },
|
|
2872
|
+
{ v: sm.edges.length, l: 'Зависимостей' },
|
|
2873
|
+
{ v: sm.pipelines.length, l: 'Пайплайнов' },
|
|
2874
|
+
{ v: sm.autodiscovery.dockerServices + sm.autodiscovery.envConnections + sm.autodiscovery.npmServices, l: 'Автодискаверинг' },
|
|
2875
|
+
sm.nodes.filter(n => n.alerts && n.alerts.length > 0).length > 0
|
|
2876
|
+
? { v: sm.nodes.reduce((s, n) => s + (n.alerts?.length || 0), 0), l: 'Алертов' }
|
|
2877
|
+
: null,
|
|
2878
|
+
].filter(Boolean);
|
|
2879
|
+
} else {
|
|
2880
|
+
items = [
|
|
2881
|
+
{ v: 0, l: 'Сервисов' },
|
|
2882
|
+
{ v: '—', l: 'Зависимостей' },
|
|
2883
|
+
{ v: '—', l: 'Пайплайнов' },
|
|
2884
|
+
];
|
|
2885
|
+
}
|
|
2798
2886
|
} else if (D.hasConfig && D.features) {
|
|
2799
2887
|
const unmapped = src.filter(m => !m.isInfra && (!m.featureKeys || m.featureKeys.length === 0)).length;
|
|
2800
2888
|
items = [
|
|
@@ -2827,6 +2915,7 @@ function renderModeSwitch() {
|
|
|
2827
2915
|
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2828
2916
|
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2829
2917
|
{ key: 'docs', label: 'Документация', hint: 'Актуальность, генерация, обновление' },
|
|
2918
|
+
{ key: 'services', label: 'Карта сервисов', hint: 'Зависимости, пайплайны, мониторинг' },
|
|
2830
2919
|
];
|
|
2831
2920
|
root.innerHTML = modes.map(m => `
|
|
2832
2921
|
<button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
|
|
@@ -2865,6 +2954,29 @@ function renderSidebar() {
|
|
|
2865
2954
|
return;
|
|
2866
2955
|
}
|
|
2867
2956
|
|
|
2957
|
+
if (contextMode === 'services') {
|
|
2958
|
+
tabs.style.display = 'none';
|
|
2959
|
+
const svcTab = modeStore.services.svcTab || 'graph';
|
|
2960
|
+
extra.innerHTML = `
|
|
2961
|
+
<div class="sidebar-label">Карта сервисов</div>
|
|
2962
|
+
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45;margin-bottom:12px">
|
|
2963
|
+
Зависимости, пайплайны, экспорт для мониторинга.
|
|
2964
|
+
</div>
|
|
2965
|
+
<div class="svc-sidebar-tabs">
|
|
2966
|
+
<button class="svc-sidebar-tab ${svcTab === 'graph' ? 'active' : ''}" data-svctab="graph">Граф</button>
|
|
2967
|
+
<button class="svc-sidebar-tab ${svcTab === 'pipelines' ? 'active' : ''}" data-svctab="pipelines">Пайплайны</button>
|
|
2968
|
+
<button class="svc-sidebar-tab ${svcTab === 'export' ? 'active' : ''}" data-svctab="export">Экспорт</button>
|
|
2969
|
+
</div>`;
|
|
2970
|
+
extra.querySelectorAll('.svc-sidebar-tab').forEach(btn => {
|
|
2971
|
+
btn.onclick = () => {
|
|
2972
|
+
modeStore.services.svcTab = btn.dataset.svctab;
|
|
2973
|
+
renderSidebar();
|
|
2974
|
+
renderContent();
|
|
2975
|
+
};
|
|
2976
|
+
});
|
|
2977
|
+
return;
|
|
2978
|
+
}
|
|
2979
|
+
|
|
2868
2980
|
tabs.style.display = 'flex';
|
|
2869
2981
|
document.querySelectorAll('.view-tab').forEach(t =>
|
|
2870
2982
|
t.classList.toggle('active', t.dataset.view === view)
|
|
@@ -2914,6 +3026,10 @@ function renderContent() {
|
|
|
2914
3026
|
renderDocumentation(c);
|
|
2915
3027
|
return;
|
|
2916
3028
|
}
|
|
3029
|
+
if (contextMode === 'services') {
|
|
3030
|
+
renderServiceMap(c);
|
|
3031
|
+
return;
|
|
3032
|
+
}
|
|
2917
3033
|
|
|
2918
3034
|
if (view === 'features') {
|
|
2919
3035
|
if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
|
|
@@ -2926,6 +3042,376 @@ function renderContent() {
|
|
|
2926
3042
|
}
|
|
2927
3043
|
}
|
|
2928
3044
|
|
|
3045
|
+
// ─── Service Map rendering ──────────────────────────────────────────────────
|
|
3046
|
+
|
|
3047
|
+
const SVC_CATEGORY_COLORS = {
|
|
3048
|
+
'database': { fill: '#1a3a2a', stroke: '#3fb950', text: '#3fb950' },
|
|
3049
|
+
'cache': { fill: '#3a2a1a', stroke: '#ffa657', text: '#ffa657' },
|
|
3050
|
+
'queue': { fill: '#2a1a3a', stroke: '#d2a8ff', text: '#d2a8ff' },
|
|
3051
|
+
'storage': { fill: '#1a2a3a', stroke: '#58a6ff', text: '#58a6ff' },
|
|
3052
|
+
'external-api': { fill: '#3a1a2a', stroke: '#f85149', text: '#f85149' },
|
|
3053
|
+
'internal-service': { fill: '#1a2a2a', stroke: '#79c0ff', text: '#79c0ff' },
|
|
3054
|
+
'worker': { fill: '#2a2a1a', stroke: '#e3b341', text: '#e3b341' },
|
|
3055
|
+
'gateway': { fill: '#2a1a2a', stroke: '#a371f7', text: '#a371f7' },
|
|
3056
|
+
};
|
|
3057
|
+
|
|
3058
|
+
const SVC_GROUP_ORDER = ['gateway', 'external', 'services', 'workers', 'queues', 'cache', 'storage', 'databases'];
|
|
3059
|
+
const SVC_GROUP_LABELS = { gateway: 'Шлюзы', external: 'Внешние API', services: 'Сервисы', workers: 'Воркеры', queues: 'Очереди', cache: 'Кэш', storage: 'Хранилище', databases: 'Базы данных' };
|
|
3060
|
+
|
|
3061
|
+
function renderServiceMap(c) {
|
|
3062
|
+
const sm = D.serviceMap;
|
|
3063
|
+
if (!sm || sm.nodes.length === 0) {
|
|
3064
|
+
c.innerHTML = `<div class="svc-empty">
|
|
3065
|
+
<h3>Карта сервисов пуста</h3>
|
|
3066
|
+
<p>Автодискаверинг не нашёл сервисов. Добавь секцию <code>services</code> в <code>viberadar.config.json</code> или убедись что в проекте есть docker-compose.yml / .env файлы.</p>
|
|
3067
|
+
</div>`;
|
|
3068
|
+
return;
|
|
3069
|
+
}
|
|
3070
|
+
const svcTab = modeStore.services.svcTab || 'graph';
|
|
3071
|
+
if (svcTab === 'graph') renderSvcGraph(c, sm);
|
|
3072
|
+
else if (svcTab === 'pipelines') renderSvcPipelines(c, sm);
|
|
3073
|
+
else if (svcTab === 'export') renderSvcExport(c, sm);
|
|
3074
|
+
}
|
|
3075
|
+
|
|
3076
|
+
function renderSvcGraph(c, sm) {
|
|
3077
|
+
// Group nodes
|
|
3078
|
+
const groups = {};
|
|
3079
|
+
for (const node of sm.nodes) {
|
|
3080
|
+
const g = node.group || 'services';
|
|
3081
|
+
if (!groups[g]) groups[g] = [];
|
|
3082
|
+
groups[g].push(node);
|
|
3083
|
+
}
|
|
3084
|
+
|
|
3085
|
+
const NODE_W = 130, NODE_H = 56, PAD_X = 40, PAD_Y = 24, GROUP_PAD = 50;
|
|
3086
|
+
const orderedGroups = SVC_GROUP_ORDER.filter(g => groups[g]?.length > 0);
|
|
3087
|
+
|
|
3088
|
+
// Calculate positions
|
|
3089
|
+
const positions = {};
|
|
3090
|
+
let maxRowW = 0;
|
|
3091
|
+
for (const g of orderedGroups) {
|
|
3092
|
+
const row = groups[g];
|
|
3093
|
+
maxRowW = Math.max(maxRowW, row.length * (NODE_W + PAD_X));
|
|
3094
|
+
}
|
|
3095
|
+
|
|
3096
|
+
const svgW = Math.max(maxRowW + PAD_X * 2, 600);
|
|
3097
|
+
let curY = GROUP_PAD;
|
|
3098
|
+
const groupYPositions = {};
|
|
3099
|
+
|
|
3100
|
+
for (const g of orderedGroups) {
|
|
3101
|
+
const row = groups[g];
|
|
3102
|
+
groupYPositions[g] = curY;
|
|
3103
|
+
const totalW = row.length * NODE_W + (row.length - 1) * PAD_X;
|
|
3104
|
+
const startX = (svgW - totalW) / 2;
|
|
3105
|
+
row.forEach((node, i) => {
|
|
3106
|
+
positions[node.id] = {
|
|
3107
|
+
x: startX + i * (NODE_W + PAD_X) + NODE_W / 2,
|
|
3108
|
+
y: curY + NODE_H / 2 + 20, // 20px for group label
|
|
3109
|
+
};
|
|
3110
|
+
});
|
|
3111
|
+
curY += NODE_H + PAD_Y + GROUP_PAD;
|
|
3112
|
+
}
|
|
3113
|
+
const svgH = curY + GROUP_PAD;
|
|
3114
|
+
|
|
3115
|
+
// Build SVG
|
|
3116
|
+
let svg = `<svg class="svc-graph-svg" viewBox="0 0 ${svgW} ${svgH}" xmlns="http://www.w3.org/2000/svg">`;
|
|
3117
|
+
svg += `<defs>
|
|
3118
|
+
<marker id="svc-arrow" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto-start-reverse">
|
|
3119
|
+
<path d="M 0 0 L 10 3.5 L 0 7 z" fill="var(--border)"/>
|
|
3120
|
+
</marker>
|
|
3121
|
+
<marker id="svc-arrow-critical" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto-start-reverse">
|
|
3122
|
+
<path d="M 0 0 L 10 3.5 L 0 7 z" fill="#f85149"/>
|
|
3123
|
+
</marker>
|
|
3124
|
+
<marker id="svc-arrow-hl" viewBox="0 0 10 7" refX="10" refY="3.5" markerWidth="8" markerHeight="6" orient="auto-start-reverse">
|
|
3125
|
+
<path d="M 0 0 L 10 3.5 L 0 7 z" fill="var(--accent)"/>
|
|
3126
|
+
</marker>
|
|
3127
|
+
</defs>`;
|
|
3128
|
+
|
|
3129
|
+
// Group labels
|
|
3130
|
+
for (const g of orderedGroups) {
|
|
3131
|
+
const y = groupYPositions[g];
|
|
3132
|
+
svg += `<text x="16" y="${y + 12}" class="svc-group-label">${SVC_GROUP_LABELS[g] || g}</text>`;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
// Edges
|
|
3136
|
+
for (const edge of sm.edges) {
|
|
3137
|
+
const from = positions[edge.from];
|
|
3138
|
+
const to = positions[edge.to];
|
|
3139
|
+
if (!from || !to) continue;
|
|
3140
|
+
const cls = edge.critical ? 'svc-edge critical' : 'svc-edge';
|
|
3141
|
+
const marker = edge.critical ? 'url(#svc-arrow-critical)' : 'url(#svc-arrow)';
|
|
3142
|
+
// Bezier curve
|
|
3143
|
+
const dy = to.y - from.y;
|
|
3144
|
+
const dx = to.x - from.x;
|
|
3145
|
+
const cp = Math.abs(dy) * 0.4;
|
|
3146
|
+
const path = `M ${from.x} ${from.y + NODE_H/2} C ${from.x} ${from.y + NODE_H/2 + cp}, ${to.x} ${to.y - NODE_H/2 - cp}, ${to.x} ${to.y - NODE_H/2}`;
|
|
3147
|
+
svg += `<path d="${path}" class="${cls}" marker-end="${marker}" data-from="${edge.from}" data-to="${edge.to}"/>`;
|
|
3148
|
+
if (edge.label) {
|
|
3149
|
+
const mx = (from.x + to.x) / 2;
|
|
3150
|
+
const my = (from.y + to.y) / 2;
|
|
3151
|
+
svg += `<text x="${mx}" y="${my}" class="svc-edge-label" text-anchor="middle">${edge.label}</text>`;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
|
|
3155
|
+
// Nodes
|
|
3156
|
+
for (const node of sm.nodes) {
|
|
3157
|
+
const pos = positions[node.id];
|
|
3158
|
+
if (!pos) continue;
|
|
3159
|
+
const colors = SVC_CATEGORY_COLORS[node.category] || SVC_CATEGORY_COLORS['internal-service'];
|
|
3160
|
+
const x = pos.x - NODE_W / 2;
|
|
3161
|
+
const y = pos.y - NODE_H / 2;
|
|
3162
|
+
svg += `<g class="svc-node-group" data-nodeid="${node.id}" transform="translate(${x},${y})">`;
|
|
3163
|
+
svg += `<rect class="svc-node-rect" width="${NODE_W}" height="${NODE_H}" fill="${colors.fill}" stroke="${colors.stroke}"/>`;
|
|
3164
|
+
svg += `<text class="svc-node-icon" x="${NODE_W/2}" y="22" text-anchor="middle">${node.icon || '📦'}</text>`;
|
|
3165
|
+
svg += `<text class="svc-node-label" x="${NODE_W/2}" y="36" text-anchor="middle">${truncateText(node.label, 16)}</text>`;
|
|
3166
|
+
svg += `<text class="svc-node-category" x="${NODE_W/2}" y="48" text-anchor="middle">${node.category}</text>`;
|
|
3167
|
+
svg += `</g>`;
|
|
3168
|
+
}
|
|
3169
|
+
|
|
3170
|
+
svg += `</svg>`;
|
|
3171
|
+
|
|
3172
|
+
// Legend
|
|
3173
|
+
const legendItems = Object.entries(SVC_CATEGORY_COLORS)
|
|
3174
|
+
.filter(([cat]) => sm.nodes.some(n => n.category === cat))
|
|
3175
|
+
.map(([cat, colors]) => `<div class="svc-legend-item"><div class="svc-legend-dot" style="background:${colors.stroke}"></div>${cat}</div>`)
|
|
3176
|
+
.join('');
|
|
3177
|
+
|
|
3178
|
+
c.innerHTML = `<div class="svc-graph-wrap">${svg}<div class="svc-legend">${legendItems}</div></div>`;
|
|
3179
|
+
|
|
3180
|
+
// Interactivity
|
|
3181
|
+
c.querySelectorAll('.svc-node-group').forEach(g => {
|
|
3182
|
+
g.addEventListener('mouseenter', () => {
|
|
3183
|
+
const id = g.dataset.nodeid;
|
|
3184
|
+
c.querySelectorAll('.svc-edge').forEach(e => {
|
|
3185
|
+
if (e.dataset.from === id || e.dataset.to === id) {
|
|
3186
|
+
e.classList.add('highlighted');
|
|
3187
|
+
e.setAttribute('marker-end', 'url(#svc-arrow-hl)');
|
|
3188
|
+
}
|
|
3189
|
+
});
|
|
3190
|
+
});
|
|
3191
|
+
g.addEventListener('mouseleave', () => {
|
|
3192
|
+
c.querySelectorAll('.svc-edge').forEach(e => {
|
|
3193
|
+
e.classList.remove('highlighted');
|
|
3194
|
+
const isCrit = e.classList.contains('critical');
|
|
3195
|
+
e.setAttribute('marker-end', isCrit ? 'url(#svc-arrow-critical)' : 'url(#svc-arrow)');
|
|
3196
|
+
});
|
|
3197
|
+
});
|
|
3198
|
+
g.addEventListener('click', () => {
|
|
3199
|
+
const id = g.dataset.nodeid;
|
|
3200
|
+
openSvcPanel(id);
|
|
3201
|
+
});
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
|
|
3205
|
+
function truncateText(text, maxLen) {
|
|
3206
|
+
return text.length > maxLen ? text.slice(0, maxLen - 1) + '…' : text;
|
|
3207
|
+
}
|
|
3208
|
+
|
|
3209
|
+
function renderSvcPipelines(c, sm) {
|
|
3210
|
+
if (!sm.pipelines || sm.pipelines.length === 0) {
|
|
3211
|
+
c.innerHTML = `<div class="svc-empty">
|
|
3212
|
+
<h3>Пайплайны не заданы</h3>
|
|
3213
|
+
<p>Добавь секцию <code>pipelines</code> в <code>services</code> конфиг, чтобы визуализировать потоки данных.</p>
|
|
3214
|
+
</div>`;
|
|
3215
|
+
return;
|
|
3216
|
+
}
|
|
3217
|
+
|
|
3218
|
+
c.innerHTML = `<div class="svc-pipeline-container">${sm.pipelines.map(p => {
|
|
3219
|
+
const triggers = (p.triggers || []).map(t => `<span class="svc-pipeline-trigger">${t}</span>`).join('');
|
|
3220
|
+
const steps = p.steps.map((s, i) => {
|
|
3221
|
+
const arrow = i < p.steps.length - 1 ? '<span class="svc-step-arrow">→</span>' : '';
|
|
3222
|
+
const svcNode = s.serviceId ? sm.nodes.find(n => n.id === s.serviceId) : null;
|
|
3223
|
+
const svcLabel = svcNode ? `${svcNode.icon || ''} ${svcNode.label}` : (s.serviceId || '');
|
|
3224
|
+
return `<div class="svc-step">
|
|
3225
|
+
<div class="svc-step-box" ${s.serviceId ? `data-svcid="${s.serviceId}"` : ''}>
|
|
3226
|
+
<div class="svc-step-label">${s.label}</div>
|
|
3227
|
+
${svcLabel ? `<div class="svc-step-service">${svcLabel}</div>` : ''}
|
|
3228
|
+
</div>
|
|
3229
|
+
</div>${arrow}`;
|
|
3230
|
+
}).join('');
|
|
3231
|
+
return `<div class="svc-pipeline">
|
|
3232
|
+
<div class="svc-pipeline-title">${p.label}</div>
|
|
3233
|
+
${p.description ? `<div class="svc-pipeline-desc">${p.description}</div>` : ''}
|
|
3234
|
+
${triggers ? `<div class="svc-pipeline-triggers">${triggers}</div>` : ''}
|
|
3235
|
+
<div class="svc-pipeline-steps">${steps}</div>
|
|
3236
|
+
</div>`;
|
|
3237
|
+
}).join('')}</div>`;
|
|
3238
|
+
|
|
3239
|
+
// Click on step to open service detail
|
|
3240
|
+
c.querySelectorAll('.svc-step-box[data-svcid]').forEach(box => {
|
|
3241
|
+
box.onclick = () => openSvcPanel(box.dataset.svcid);
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
|
|
3245
|
+
function renderSvcExport(c, sm) {
|
|
3246
|
+
// Services table
|
|
3247
|
+
let servicesTable = `<table class="svc-export-table">
|
|
3248
|
+
<thead><tr><th>Сервис</th><th>Категория</th><th>Host</th><th>Port</th><th>Источник</th><th>Health Check</th></tr></thead>
|
|
3249
|
+
<tbody>${sm.nodes.map(n => `<tr>
|
|
3250
|
+
<td>${n.icon || ''} ${n.label}</td>
|
|
3251
|
+
<td>${n.category}</td>
|
|
3252
|
+
<td>${n.host || '—'}</td>
|
|
3253
|
+
<td>${n.port || '—'}</td>
|
|
3254
|
+
<td>${n.source}</td>
|
|
3255
|
+
<td>${n.healthCheck ? `${n.healthCheck.type}: ${n.healthCheck.target}` : '—'}</td>
|
|
3256
|
+
</tr>`).join('')}</tbody>
|
|
3257
|
+
</table>`;
|
|
3258
|
+
|
|
3259
|
+
// Dependencies table
|
|
3260
|
+
let depsTable = '';
|
|
3261
|
+
if (sm.edges.length > 0) {
|
|
3262
|
+
depsTable = `<div class="svc-export-section">
|
|
3263
|
+
<div class="svc-export-title">Зависимости</div>
|
|
3264
|
+
<table class="svc-export-table">
|
|
3265
|
+
<thead><tr><th>From</th><th>To</th><th>Тип</th><th>Описание</th><th>Critical</th></tr></thead>
|
|
3266
|
+
<tbody>${sm.edges.map(e => `<tr>
|
|
3267
|
+
<td>${e.from}</td><td>${e.to}</td><td>${e.type}</td><td>${e.label || '—'}</td>
|
|
3268
|
+
<td>${e.critical ? '<span style="color:#f85149">●</span> Да' : '—'}</td>
|
|
3269
|
+
</tr>`).join('')}</tbody>
|
|
3270
|
+
</table>
|
|
3271
|
+
</div>`;
|
|
3272
|
+
}
|
|
3273
|
+
|
|
3274
|
+
// Alerts table
|
|
3275
|
+
let alertsTable = '';
|
|
3276
|
+
const allAlerts = sm.nodes.flatMap(n => (n.alerts || []).map(a => ({ ...a, service: n.label, serviceId: n.id })));
|
|
3277
|
+
if (allAlerts.length > 0) {
|
|
3278
|
+
alertsTable = `<div class="svc-export-section">
|
|
3279
|
+
<div class="svc-export-title">Рекомендации по алертам</div>
|
|
3280
|
+
<table class="svc-export-table">
|
|
3281
|
+
<thead><tr><th>Сервис</th><th>Метрика</th><th>Severity</th><th>Описание</th></tr></thead>
|
|
3282
|
+
<tbody>${allAlerts.map(a => `<tr>
|
|
3283
|
+
<td>${a.service}</td><td><code>${a.metric}</code></td>
|
|
3284
|
+
<td><span class="svc-alert-badge ${a.severity}">${a.severity}</span></td>
|
|
3285
|
+
<td>${a.description}</td>
|
|
3286
|
+
</tr>`).join('')}</tbody>
|
|
3287
|
+
</table>
|
|
3288
|
+
</div>`;
|
|
3289
|
+
}
|
|
3290
|
+
|
|
3291
|
+
c.innerHTML = `<div class="svc-export">
|
|
3292
|
+
<div class="svc-export-section">
|
|
3293
|
+
<div class="svc-export-title">Сервисы</div>
|
|
3294
|
+
${servicesTable}
|
|
3295
|
+
</div>
|
|
3296
|
+
${depsTable}
|
|
3297
|
+
${alertsTable}
|
|
3298
|
+
<div class="svc-export-btns">
|
|
3299
|
+
<button class="svc-export-btn" id="svcCopyJson">📋 Copy JSON</button>
|
|
3300
|
+
<button class="svc-export-btn" id="svcCopyMd">📄 Copy Markdown</button>
|
|
3301
|
+
<button class="svc-export-btn" id="svcCopyProm">⚡ Copy Prometheus Rules</button>
|
|
3302
|
+
</div>
|
|
3303
|
+
</div>`;
|
|
3304
|
+
|
|
3305
|
+
document.getElementById('svcCopyJson').onclick = async () => {
|
|
3306
|
+
try {
|
|
3307
|
+
const res = await fetch('/api/service-map?format=json');
|
|
3308
|
+
const text = await res.text();
|
|
3309
|
+
await navigator.clipboard.writeText(text);
|
|
3310
|
+
showToast('JSON скопирован в буфер обмена');
|
|
3311
|
+
} catch (e) { showToast('Ошибка копирования'); }
|
|
3312
|
+
};
|
|
3313
|
+
document.getElementById('svcCopyMd').onclick = async () => {
|
|
3314
|
+
try {
|
|
3315
|
+
const res = await fetch('/api/service-map?format=markdown');
|
|
3316
|
+
const text = await res.text();
|
|
3317
|
+
await navigator.clipboard.writeText(text);
|
|
3318
|
+
showToast('Markdown скопирован в буфер обмена');
|
|
3319
|
+
} catch (e) { showToast('Ошибка копирования'); }
|
|
3320
|
+
};
|
|
3321
|
+
document.getElementById('svcCopyProm').onclick = async () => {
|
|
3322
|
+
try {
|
|
3323
|
+
const res = await fetch('/api/service-map?format=prometheus');
|
|
3324
|
+
const text = await res.text();
|
|
3325
|
+
await navigator.clipboard.writeText(text);
|
|
3326
|
+
showToast('Prometheus rules скопированы в буфер обмена');
|
|
3327
|
+
} catch (e) { showToast('Ошибка копирования'); }
|
|
3328
|
+
};
|
|
3329
|
+
}
|
|
3330
|
+
|
|
3331
|
+
function showToast(msg) {
|
|
3332
|
+
let toast = document.getElementById('svc-toast');
|
|
3333
|
+
if (!toast) {
|
|
3334
|
+
toast = document.createElement('div');
|
|
3335
|
+
toast.id = 'svc-toast';
|
|
3336
|
+
toast.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:var(--surface);color:var(--text);padding:8px 20px;border-radius:6px;border:1px solid var(--border);font-size:12px;z-index:9999;opacity:0;transition:opacity 0.3s;';
|
|
3337
|
+
document.body.appendChild(toast);
|
|
3338
|
+
}
|
|
3339
|
+
toast.textContent = msg;
|
|
3340
|
+
toast.style.opacity = '1';
|
|
3341
|
+
setTimeout(() => { toast.style.opacity = '0'; }, 2000);
|
|
3342
|
+
}
|
|
3343
|
+
|
|
3344
|
+
function openSvcPanel(nodeId) {
|
|
3345
|
+
const sm = D.serviceMap;
|
|
3346
|
+
if (!sm) return;
|
|
3347
|
+
const node = sm.nodes.find(n => n.id === nodeId);
|
|
3348
|
+
if (!node) return;
|
|
3349
|
+
|
|
3350
|
+
const panel = document.getElementById('panel');
|
|
3351
|
+
const colors = SVC_CATEGORY_COLORS[node.category] || SVC_CATEGORY_COLORS['internal-service'];
|
|
3352
|
+
|
|
3353
|
+
// Incoming/outgoing edges
|
|
3354
|
+
const incoming = sm.edges.filter(e => e.to === nodeId);
|
|
3355
|
+
const outgoing = sm.edges.filter(e => e.from === nodeId);
|
|
3356
|
+
|
|
3357
|
+
// Pipelines this node participates in
|
|
3358
|
+
const pipelines = sm.pipelines.filter(p => p.steps.some(s => s.serviceId === nodeId));
|
|
3359
|
+
|
|
3360
|
+
let html = `<div style="padding:16px">
|
|
3361
|
+
<div style="display:flex;align-items:center;gap:10px;margin-bottom:16px">
|
|
3362
|
+
<span style="font-size:28px">${node.icon || '📦'}</span>
|
|
3363
|
+
<div>
|
|
3364
|
+
<div style="font-size:16px;font-weight:600;color:var(--text)">${node.label}</div>
|
|
3365
|
+
<div style="font-size:11px;color:${colors.text}">${node.category}</div>
|
|
3366
|
+
</div>
|
|
3367
|
+
</div>
|
|
3368
|
+
|
|
3369
|
+
<div class="svc-detail-section">
|
|
3370
|
+
<h4>Информация</h4>
|
|
3371
|
+
<div class="svc-detail-row">ID: <code>${node.id}</code></div>
|
|
3372
|
+
<div class="svc-detail-row">Источник: <span class="svc-detail-badge" style="background:${node.source === 'config' ? 'rgba(88,166,255,0.15)' : 'rgba(63,185,80,0.15)'};color:${node.source === 'config' ? '#58a6ff' : '#3fb950'}">${node.source}</span></div>
|
|
3373
|
+
${node.host ? `<div class="svc-detail-row">Host: <code>${node.host}</code></div>` : ''}
|
|
3374
|
+
${node.port ? `<div class="svc-detail-row">Port: <code>${node.port}</code></div>` : ''}
|
|
3375
|
+
${node.group ? `<div class="svc-detail-row">Группа: ${node.group}</div>` : ''}
|
|
3376
|
+
</div>`;
|
|
3377
|
+
|
|
3378
|
+
if (incoming.length > 0) {
|
|
3379
|
+
html += `<div class="svc-detail-section"><h4>Входящие зависимости (${incoming.length})</h4>
|
|
3380
|
+
${incoming.map(e => `<div class="svc-detail-row">${e.from} → <strong>${nodeId}</strong> ${e.label ? `(${e.label})` : ''} <span style="color:var(--muted)">${e.type}</span>${e.critical ? ' <span style="color:#f85149">critical</span>' : ''}</div>`).join('')}
|
|
3381
|
+
</div>`;
|
|
3382
|
+
}
|
|
3383
|
+
if (outgoing.length > 0) {
|
|
3384
|
+
html += `<div class="svc-detail-section"><h4>Исходящие зависимости (${outgoing.length})</h4>
|
|
3385
|
+
${outgoing.map(e => `<div class="svc-detail-row"><strong>${nodeId}</strong> → ${e.to} ${e.label ? `(${e.label})` : ''} <span style="color:var(--muted)">${e.type}</span>${e.critical ? ' <span style="color:#f85149">critical</span>' : ''}</div>`).join('')}
|
|
3386
|
+
</div>`;
|
|
3387
|
+
}
|
|
3388
|
+
if (pipelines.length > 0) {
|
|
3389
|
+
html += `<div class="svc-detail-section"><h4>Участвует в пайплайнах (${pipelines.length})</h4>
|
|
3390
|
+
${pipelines.map(p => {
|
|
3391
|
+
const stepIdx = p.steps.findIndex(s => s.serviceId === nodeId);
|
|
3392
|
+
return `<div class="svc-detail-row">📊 ${p.label} — шаг ${stepIdx + 1}: ${p.steps[stepIdx].label}</div>`;
|
|
3393
|
+
}).join('')}
|
|
3394
|
+
</div>`;
|
|
3395
|
+
}
|
|
3396
|
+
if (node.healthCheck) {
|
|
3397
|
+
html += `<div class="svc-detail-section"><h4>Health Check</h4>
|
|
3398
|
+
<div class="svc-detail-row">Тип: ${node.healthCheck.type}</div>
|
|
3399
|
+
<div class="svc-detail-row">Target: <code>${node.healthCheck.target}</code></div>
|
|
3400
|
+
${node.healthCheck.interval ? `<div class="svc-detail-row">Интервал: ${node.healthCheck.interval}</div>` : ''}
|
|
3401
|
+
</div>`;
|
|
3402
|
+
}
|
|
3403
|
+
if (node.alerts && node.alerts.length > 0) {
|
|
3404
|
+
html += `<div class="svc-detail-section"><h4>Алерты (${node.alerts.length})</h4>
|
|
3405
|
+
${node.alerts.map(a => `<div class="svc-detail-row"><span class="svc-alert-badge ${a.severity}">${a.severity}</span> <code>${a.metric}</code> — ${a.description}</div>`).join('')}
|
|
3406
|
+
</div>`;
|
|
3407
|
+
}
|
|
3408
|
+
|
|
3409
|
+
html += `</div>`;
|
|
3410
|
+
|
|
3411
|
+
document.getElementById('panelContent').innerHTML = html;
|
|
3412
|
+
panel.classList.add('open');
|
|
3413
|
+
}
|
|
3414
|
+
|
|
2929
3415
|
function renderQaOnboarding() {
|
|
2930
3416
|
return `<div class="onboarding-block"><h3>QA Coverage: что это?</h3><p>Этот экран помогает найти пробелы в тестах: сначала проверь покрытие по фичам, затем открой критичные непокрытые файлы и запусти генерацию/фиксы тестов.</p></div>`;
|
|
2931
3417
|
}
|
|
@@ -4948,6 +5434,14 @@ function renderTestNavSidebar(extra) {
|
|
|
4948
5434
|
<span class="tn-filter-count ${f.cls || ''}">${f.count}</span>
|
|
4949
5435
|
</div>
|
|
4950
5436
|
`).join('')}
|
|
5437
|
+
${D.agent && problemCounts['no-source'] > 0 ? `
|
|
5438
|
+
<button id="tnLinkOrphanBtn" style="margin:8px 0 2px;padding:5px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:5px;color:var(--accent);font-size:11px;cursor:pointer;width:100%;text-align:left;">
|
|
5439
|
+
▶ Связать с исходниками (${problemCounts['no-source']})
|
|
5440
|
+
</button>` : ''}
|
|
5441
|
+
${D.agent && problemCounts['no-feature'] > 0 ? `
|
|
5442
|
+
<button id="tnClassifyOrphanBtn" style="margin:2px 0 0;padding:5px 10px;background:var(--bg-card);border:1px solid var(--border);border-radius:5px;color:var(--accent);font-size:11px;cursor:pointer;width:100%;text-align:left;">
|
|
5443
|
+
▶ Привязать к фичам (${problemCounts['no-feature']})
|
|
5444
|
+
</button>` : ''}
|
|
4951
5445
|
</div>
|
|
4952
5446
|
<div class="tn-sidebar-section">
|
|
4953
5447
|
<div class="tn-sidebar-label">Фича</div>
|
|
@@ -4987,6 +5481,10 @@ function renderTestNavSidebar(extra) {
|
|
|
4987
5481
|
renderSidebar(); renderContent();
|
|
4988
5482
|
};
|
|
4989
5483
|
});
|
|
5484
|
+
const linkBtn = document.getElementById('tnLinkOrphanBtn');
|
|
5485
|
+
if (linkBtn) linkBtn.onclick = () => runAgentTask('link-orphan-tests', null, null, null, { batch: 0 });
|
|
5486
|
+
const classifyBtn = document.getElementById('tnClassifyOrphanBtn');
|
|
5487
|
+
if (classifyBtn) classifyBtn.onclick = () => runAgentTask('classify-orphan-tests', null, null, null, { batch: 0 });
|
|
4990
5488
|
}
|
|
4991
5489
|
|
|
4992
5490
|
function renderTestNavigator(c) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "viberadar",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.115",
|
|
4
4
|
"description": "Live module map with test coverage for vibecoding projects",
|
|
5
5
|
"main": "./dist/cli.js",
|
|
6
6
|
"bin": {
|
|
@@ -30,14 +30,16 @@
|
|
|
30
30
|
"viberadar.js"
|
|
31
31
|
],
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"open": "^10.1.0",
|
|
34
33
|
"chokidar": "^3.6.0",
|
|
35
|
-
"glob": "^11.0.0"
|
|
34
|
+
"glob": "^11.0.0",
|
|
35
|
+
"js-yaml": "^4.1.1",
|
|
36
|
+
"open": "^10.1.0"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
39
|
+
"@types/js-yaml": "^4.0.9",
|
|
38
40
|
"@types/node": "^22.0.0",
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
+
"ts-node": "^10.9.2",
|
|
42
|
+
"typescript": "^5.7.0"
|
|
41
43
|
},
|
|
42
44
|
"engines": {
|
|
43
45
|
"node": ">=18"
|