viberadar 0.3.230 → 0.3.232

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.
@@ -330,8 +330,116 @@
330
330
  .obs-title { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: 0.4px; margin-bottom: 10px; }
331
331
  .obs-value { font-size: 24px; font-weight: 700; margin-bottom: 4px; }
332
332
  .obs-sub { font-size: 12px; color: var(--muted); line-height: 1.4; }
333
- .obs-list { display: flex; flex-direction: column; gap: 6px; }
334
- .obs-list-item { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; }
333
+ .obs-list { display: flex; flex-direction: column; gap: 6px; }
334
+ .obs-list-item { display: flex; justify-content: space-between; gap: 8px; font-size: 12px; }
335
+
336
+ /* ── Microservices readiness ────────────────────────────────────────────── */
337
+ .micro-summary-grid {
338
+ display: grid;
339
+ grid-template-columns: repeat(auto-fit, minmax(210px, 1fr));
340
+ gap: 12px;
341
+ margin-bottom: 14px;
342
+ }
343
+ .micro-panel {
344
+ background: var(--bg-card);
345
+ border: 1px solid var(--border);
346
+ border-radius: 8px;
347
+ padding: 14px;
348
+ }
349
+ .micro-panel-title {
350
+ font-size: 12px;
351
+ color: var(--muted);
352
+ text-transform: uppercase;
353
+ letter-spacing: 0.4px;
354
+ margin-bottom: 10px;
355
+ }
356
+ .micro-risk-list { display: flex; flex-direction: column; gap: 8px; }
357
+ .micro-risk-item {
358
+ display: grid;
359
+ grid-template-columns: minmax(0, 1fr) auto;
360
+ gap: 8px;
361
+ align-items: center;
362
+ font-size: 12px;
363
+ color: var(--muted);
364
+ padding: 7px 0;
365
+ border-bottom: 1px dashed var(--border);
366
+ }
367
+ .micro-risk-item:last-child { border-bottom: none; }
368
+ .micro-path { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text); }
369
+ .micro-pill-row { display: flex; flex-wrap: wrap; gap: 5px; }
370
+ .micro-pill {
371
+ border: 1px solid var(--border);
372
+ border-radius: 999px;
373
+ padding: 1px 7px;
374
+ font-size: 10px;
375
+ color: var(--muted);
376
+ background: var(--bg);
377
+ white-space: nowrap;
378
+ }
379
+ .micro-pill.high { color: var(--red); border-color: rgba(248,81,73,0.45); }
380
+ .micro-pill.medium { color: var(--yellow); border-color: rgba(227,179,65,0.45); }
381
+ .micro-pill.ready { color: var(--green); border-color: rgba(63,185,80,0.45); }
382
+ .micro-pill.watch { color: var(--yellow); border-color: rgba(227,179,65,0.45); }
383
+ .micro-pill.risky { color: var(--red); border-color: rgba(248,81,73,0.45); }
384
+ .micro-score {
385
+ font-size: 22px;
386
+ font-weight: 700;
387
+ color: var(--blue);
388
+ margin-bottom: 4px;
389
+ }
390
+ .micro-score-bar {
391
+ height: 6px;
392
+ background: var(--border);
393
+ border-radius: 999px;
394
+ overflow: hidden;
395
+ margin-top: 8px;
396
+ }
397
+ .micro-score-fill { height: 100%; background: var(--blue); border-radius: 999px; }
398
+ .micro-table {
399
+ width: 100%;
400
+ border-collapse: collapse;
401
+ font-size: 12px;
402
+ background: var(--bg-card);
403
+ border: 1px solid var(--border);
404
+ border-radius: 8px;
405
+ overflow: hidden;
406
+ }
407
+ .micro-table th,
408
+ .micro-table td {
409
+ text-align: left;
410
+ padding: 9px 10px;
411
+ border-bottom: 1px solid var(--border);
412
+ vertical-align: top;
413
+ }
414
+ .micro-table th {
415
+ color: var(--muted);
416
+ text-transform: uppercase;
417
+ font-size: 10px;
418
+ letter-spacing: 0.4px;
419
+ background: var(--bg);
420
+ }
421
+ .micro-table tr:last-child td { border-bottom: none; }
422
+ .micro-table tr.clickable { cursor: pointer; }
423
+ .micro-table tr.clickable:hover { background: var(--bg-hover); }
424
+ .micro-section-head {
425
+ display: flex;
426
+ align-items: center;
427
+ justify-content: space-between;
428
+ gap: 10px;
429
+ margin: 18px 0 10px;
430
+ }
431
+ .micro-section-head h3 { font-size: 16px; }
432
+ .micro-subtle { font-size: 12px; color: var(--muted); line-height: 1.45; }
433
+ .micro-back {
434
+ border: 1px solid var(--border);
435
+ background: var(--bg-card);
436
+ color: var(--text);
437
+ border-radius: 6px;
438
+ padding: 6px 10px;
439
+ cursor: pointer;
440
+ font-size: 12px;
441
+ }
442
+ .micro-back:hover { background: var(--bg-hover); border-color: var(--blue); }
335
443
 
336
444
  /* ── Feature cards ───────────────────────────────────────────────────────── */
337
445
  .features-grid {
@@ -1991,6 +2099,12 @@ let taskImportDraft = '';
1991
2099
  let taskImportResult = '';
1992
2100
  let taskSelectedId = null;
1993
2101
  let taskEditMode = false;
2102
+ let microFeatureFilter = 'all';
2103
+ let microTierFilter = 'all';
2104
+ let microSizeFilter = 'problem';
2105
+ let microTypeFilter = 'all';
2106
+ let microSearchQuery = '';
2107
+ let microDrillKey = null;
1994
2108
 
1995
2109
  function toggleObsHint(id) {
1996
2110
  document.getElementById(id).classList.toggle('open');
@@ -2007,8 +2121,9 @@ function switchObsTab(tabId) {
2007
2121
  }
2008
2122
  const modeStore = {
2009
2123
  qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
2010
- observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
2011
- docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
2124
+ observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
2125
+ microservices: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
2126
+ docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
2012
2127
  scenarios: { view: 'list', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
2013
2128
  services: { view: 'graph', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null, svcTab: 'graph' },
2014
2129
  load: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false, testNavType: 'all', testNavFeature: null, testNavProblem: null },
@@ -2018,6 +2133,7 @@ const modeStore = {
2018
2133
 
2019
2134
  function getModeFromPath(pathname = window.location.pathname) {
2020
2135
  if (pathname.startsWith('/radar/observability')) return 'observability';
2136
+ if (pathname.startsWith('/radar/microservices')) return 'microservices';
2021
2137
  if (pathname.startsWith('/radar/docs')) return 'docs';
2022
2138
  if (pathname.startsWith('/radar/services')) return 'services';
2023
2139
  if (pathname.startsWith('/radar/load')) return 'load';
@@ -2028,6 +2144,7 @@ function getModeFromPath(pathname = window.location.pathname) {
2028
2144
 
2029
2145
  function routePathForMode(mode) {
2030
2146
  if (mode === 'observability') return '/radar/observability';
2147
+ if (mode === 'microservices') return '/radar/microservices';
2031
2148
  if (mode === 'docs') return '/radar/docs';
2032
2149
  if (mode === 'services') return '/radar/services';
2033
2150
  if (mode === 'load') return '/radar/load';
@@ -2076,7 +2193,7 @@ function switchMode(nextMode) {
2076
2193
  saveModeState(contextMode);
2077
2194
  contextMode = nextMode;
2078
2195
  restoreModeState(contextMode);
2079
- if (contextMode === 'observability' || contextMode === 'docs' || contextMode === 'services' || contextMode === 'load' || contextMode === 'probe' || contextMode === 'tasks') {
2196
+ if (contextMode === 'observability' || contextMode === 'microservices' || contextMode === 'docs' || contextMode === 'services' || contextMode === 'load' || contextMode === 'probe' || contextMode === 'tasks') {
2080
2197
  view = 'features';
2081
2198
  drillFeatureKey = null;
2082
2199
  drillTestType = null;
@@ -3393,8 +3510,8 @@ function renderStats() {
3393
3510
  const pct = src.length ? Math.round(tested / src.length * 100) : 0;
3394
3511
 
3395
3512
  let items;
3396
- if (contextMode === 'observability') {
3397
- const o = D.observability;
3513
+ if (contextMode === 'observability') {
3514
+ const o = D.observability;
3398
3515
  if (o) {
3399
3516
  const noiseRatio = Math.round(o.metrics.noise_ratio * 100);
3400
3517
  const structPct = Math.round(o.metrics.structured_completeness * 100);
@@ -3417,8 +3534,25 @@ function renderStats() {
3417
3534
  { v: '—', l: 'Структурированность' },
3418
3535
  { v: '—', l: 'Нет покрытия' },
3419
3536
  { v: '—', l: 'Шумных паттернов' },
3420
- ];
3421
- }
3537
+ ];
3538
+ }
3539
+ } else if (contextMode === 'microservices') {
3540
+ const ms = D.microservices;
3541
+ if (ms) {
3542
+ items = [
3543
+ { v: ms.summary.godFiles + ms.summary.criticalFiles, l: 'God files', c: (ms.summary.godFiles + ms.summary.criticalFiles) ? '#f85149' : '#3fb950' },
3544
+ { v: ms.summary.largeFiles, l: 'Large files', c: ms.summary.largeFiles ? '#e3b341' : undefined },
3545
+ { v: ms.summary.modules, l: 'Modules' },
3546
+ { v: ms.summary.readyModules, l: 'Ready >= 70', c: '#3fb950' },
3547
+ { v: ms.summary.riskyModules, l: 'Risky < 40', c: ms.summary.riskyModules ? '#f85149' : undefined },
3548
+ ];
3549
+ } else {
3550
+ items = [
3551
+ { v: '—', l: 'God files' },
3552
+ { v: '—', l: 'Large files' },
3553
+ { v: '—', l: 'Modules' },
3554
+ ];
3555
+ }
3422
3556
  } else if (contextMode === 'tasks') {
3423
3557
  const tasks = taskTracker.tasks || [];
3424
3558
  const active = tasks.filter(t => t.status !== 'archived');
@@ -3478,10 +3612,11 @@ function renderStats() {
3478
3612
 
3479
3613
  function renderModeSwitch() {
3480
3614
  const root = document.getElementById('modeSwitch');
3481
- const modes = [
3482
- { key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
3483
- { key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
3484
- { key: 'docs', label: 'Документация', hint: 'Актуальность, генерация, обновление' },
3615
+ const modes = [
3616
+ { key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
3617
+ { key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
3618
+ { key: 'microservices', label: 'Микросервисы', hint: 'God files, границы, готовность' },
3619
+ { key: 'docs', label: 'Документация', hint: 'Актуальность, генерация, обновление' },
3485
3620
  { key: 'scenarios', label: 'Сценарии', hint: 'Пользовательские сценарии, user journeys' },
3486
3621
  { key: 'services', label: 'Карта сервисов', hint: 'Зависимости, пайплайны, мониторинг' },
3487
3622
  { key: 'load', label: 'Нагрузка', hint: 'k6: метрики, сценарии, AI-анализ' },
@@ -3515,17 +3650,71 @@ function renderSidebar() {
3515
3650
  return;
3516
3651
  }
3517
3652
 
3518
- if (contextMode === 'observability') {
3519
- tabs.style.display = 'none';
3520
- extra.innerHTML = `
3521
- <div class="sidebar-label">Фокус наблюдаемости</div>
3522
- <div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
3523
- Источники логов и качество сигналов для триажа инцидентов.
3524
- </div>`;
3525
- return;
3526
- }
3527
-
3528
- if (contextMode === 'docs') {
3653
+ if (contextMode === 'observability') {
3654
+ tabs.style.display = 'none';
3655
+ extra.innerHTML = `
3656
+ <div class="sidebar-label">Фокус наблюдаемости</div>
3657
+ <div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
3658
+ Источники логов и качество сигналов для триажа инцидентов.
3659
+ </div>`;
3660
+ return;
3661
+ }
3662
+
3663
+ if (contextMode === 'microservices') {
3664
+ tabs.style.display = 'none';
3665
+ const ms = D.microservices;
3666
+ const features = D.features || [];
3667
+ const types = ms ? Array.from(new Set(ms.files.map(f => f.type))).sort() : [];
3668
+ extra.innerHTML = `
3669
+ <div class="sidebar-label">Микросервисы</div>
3670
+ <div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45;margin-bottom:12px">
3671
+ Наблюдение за размером файлов, границами фич и готовностью к сервисному выделению.
3672
+ </div>
3673
+ <div style="padding:0 6px;display:flex;flex-direction:column;gap:8px">
3674
+ <label class="sidebar-label" style="padding:0">Поиск</label>
3675
+ <input id="microSearchInput" class="search-input" placeholder="Файл или модуль" value="${escapeHtml(microSearchQuery)}">
3676
+ <label class="sidebar-label" style="padding:0">Фича</label>
3677
+ <select id="microFeatureFilter" class="search-input">
3678
+ <option value="all" ${microFeatureFilter === 'all' ? 'selected' : ''}>Все фичи</option>
3679
+ <option value="__unknown__" ${microFeatureFilter === '__unknown__' ? 'selected' : ''}>Unknown ownership</option>
3680
+ ${features.map(f => `<option value="${escapeHtml(f.key)}" ${microFeatureFilter === f.key ? 'selected' : ''}>${escapeHtml(f.label)}</option>`).join('')}
3681
+ </select>
3682
+ <label class="sidebar-label" style="padding:0">Готовность</label>
3683
+ <select id="microTierFilter" class="search-input">
3684
+ <option value="all" ${microTierFilter === 'all' ? 'selected' : ''}>Любая</option>
3685
+ <option value="ready" ${microTierFilter === 'ready' ? 'selected' : ''}>Ready >= 70</option>
3686
+ <option value="watch" ${microTierFilter === 'watch' ? 'selected' : ''}>Watch 40-69</option>
3687
+ <option value="risky" ${microTierFilter === 'risky' ? 'selected' : ''}>Risky < 40</option>
3688
+ </select>
3689
+ <label class="sidebar-label" style="padding:0">Размер</label>
3690
+ <select id="microSizeFilter" class="search-input">
3691
+ <option value="problem" ${microSizeFilter === 'problem' ? 'selected' : ''}>Large + god + critical</option>
3692
+ <option value="all" ${microSizeFilter === 'all' ? 'selected' : ''}>Все</option>
3693
+ <option value="critical" ${microSizeFilter === 'critical' ? 'selected' : ''}>Critical</option>
3694
+ <option value="god" ${microSizeFilter === 'god' ? 'selected' : ''}>God</option>
3695
+ <option value="large" ${microSizeFilter === 'large' ? 'selected' : ''}>Large</option>
3696
+ <option value="normal" ${microSizeFilter === 'normal' ? 'selected' : ''}>Normal</option>
3697
+ </select>
3698
+ <label class="sidebar-label" style="padding:0">Тип файла</label>
3699
+ <select id="microTypeFilter" class="search-input">
3700
+ <option value="all" ${microTypeFilter === 'all' ? 'selected' : ''}>Все типы</option>
3701
+ ${types.map(t => `<option value="${escapeHtml(t)}" ${microTypeFilter === t ? 'selected' : ''}>${escapeHtml(t)}</option>`).join('')}
3702
+ </select>
3703
+ ${ms ? `<div class="micro-subtle" style="margin-top:6px">
3704
+ Source: <span style="color:var(--text)">${ms.summary.sourceFiles}</span><br>
3705
+ Unknown: <span style="color:var(--text)">${ms.summary.unmappedFiles}</span><br>
3706
+ Multi-feature: <span style="color:var(--text)">${ms.summary.multiFeatureFiles}</span>
3707
+ </div>` : ''}
3708
+ </div>`;
3709
+ document.getElementById('microSearchInput').oninput = (e) => { microSearchQuery = e.target.value; renderContent(); };
3710
+ document.getElementById('microFeatureFilter').onchange = (e) => { microFeatureFilter = e.target.value; microDrillKey = null; renderStats(); renderContent(); };
3711
+ document.getElementById('microTierFilter').onchange = (e) => { microTierFilter = e.target.value; microDrillKey = null; renderStats(); renderContent(); };
3712
+ document.getElementById('microSizeFilter').onchange = (e) => { microSizeFilter = e.target.value; renderContent(); };
3713
+ document.getElementById('microTypeFilter').onchange = (e) => { microTypeFilter = e.target.value; renderContent(); };
3714
+ return;
3715
+ }
3716
+
3717
+ if (contextMode === 'docs') {
3529
3718
  tabs.style.display = 'none';
3530
3719
  extra.innerHTML = `
3531
3720
  <div class="sidebar-label">Документация</div>
@@ -3690,17 +3879,288 @@ function renderSidebar() {
3690
3879
  renderSidebar();
3691
3880
  renderContent();
3692
3881
  };
3693
- });
3694
- }
3695
-
3696
- // ─── Content ──────────────────────────────────────────────────────────────────
3882
+ });
3883
+ }
3884
+
3885
+ // ─── Microservices readiness ─────────────────────────────────────────────────
3886
+ function microCategoryLabel(cat) {
3887
+ return cat === 'critical' ? 'Critical' : cat === 'god' ? 'God' : cat === 'large' ? 'Large' : 'Normal';
3888
+ }
3889
+
3890
+ function microTierLabel(tier) {
3891
+ return tier === 'ready' ? 'Ready' : tier === 'risky' ? 'Risky' : 'Watch';
3892
+ }
3893
+
3894
+ function microScoreColor(score) {
3895
+ if (score >= 70) return 'var(--green)';
3896
+ if (score < 40) return 'var(--red)';
3897
+ return 'var(--yellow)';
3898
+ }
3899
+
3900
+ function microRiskPills(risks, max = 3) {
3901
+ const shown = (risks || []).slice(0, max);
3902
+ const rest = Math.max(0, (risks || []).length - shown.length);
3903
+ return `
3904
+ <div class="micro-pill-row">
3905
+ ${shown.map(r => `<span class="micro-pill ${r.severity}">${escapeHtml(r.label)}</span>`).join('')}
3906
+ ${rest ? `<span class="micro-pill">+${rest}</span>` : ''}
3907
+ </div>`;
3908
+ }
3909
+
3910
+ function microFeatureLabels(keys) {
3911
+ if (!keys || keys.length === 0) return '<span class="micro-pill medium">unknown</span>';
3912
+ const byKey = new Map((D.features || []).map(f => [f.key, f.label]));
3913
+ return `<div class="micro-pill-row">${keys.map(k => `<span class="micro-pill">${escapeHtml(byKey.get(k) || k)}</span>`).join('')}</div>`;
3914
+ }
3915
+
3916
+ function filteredMicroModules(ms) {
3917
+ const q = microSearchQuery.trim().toLowerCase();
3918
+ return (ms.modules || []).filter(m => {
3919
+ if (microFeatureFilter !== 'all' && m.key !== microFeatureFilter) return false;
3920
+ if (microTierFilter !== 'all' && m.readinessTier !== microTierFilter) return false;
3921
+ if (q && !(`${m.key} ${m.label} ${m.description}`.toLowerCase().includes(q))) return false;
3922
+ return true;
3923
+ });
3924
+ }
3925
+
3926
+ function filteredMicroFiles(ms, moduleKey = null) {
3927
+ const q = microSearchQuery.trim().toLowerCase();
3928
+ return (ms.files || []).filter(f => {
3929
+ if (moduleKey) {
3930
+ if (moduleKey === '__unknown__') {
3931
+ if (f.featureKeys.length !== 0) return false;
3932
+ } else if (!f.featureKeys.includes(moduleKey)) return false;
3933
+ } else if (microFeatureFilter !== 'all') {
3934
+ if (microFeatureFilter === '__unknown__') {
3935
+ if (f.featureKeys.length !== 0) return false;
3936
+ } else if (!f.featureKeys.includes(microFeatureFilter)) return false;
3937
+ }
3938
+ if (microSizeFilter === 'problem' && f.sizeCategory === 'normal') return false;
3939
+ if (microSizeFilter !== 'all' && microSizeFilter !== 'problem' && f.sizeCategory !== microSizeFilter) return false;
3940
+ if (microTypeFilter !== 'all' && f.type !== microTypeFilter) return false;
3941
+ if (q && !(`${f.path} ${f.type} ${(f.featureKeys || []).join(' ')} ${(f.risks || []).map(r => r.label).join(' ')}`.toLowerCase().includes(q))) return false;
3942
+ return true;
3943
+ });
3944
+ }
3945
+
3946
+ function renderMicroservices(c) {
3947
+ const ms = D.microservices;
3948
+ if (!ms) {
3949
+ c.innerHTML = `<div class="empty-state">Нет данных microservices readiness. Обновите сканирование проекта.</div>`;
3950
+ return;
3951
+ }
3952
+ if (microDrillKey) {
3953
+ renderMicroserviceDetail(c, ms, microDrillKey);
3954
+ return;
3955
+ }
3956
+
3957
+ const modules = filteredMicroModules(ms);
3958
+ const files = filteredMicroFiles(ms);
3959
+ const problemFiles = files.filter(f => f.sizeCategory !== 'normal');
3960
+ const riskFiles = (ms.topRisks || []).slice(0, 6);
3961
+
3962
+ c.innerHTML = `
3963
+ <div class="micro-summary-grid">
3964
+ <div class="micro-panel">
3965
+ <div class="micro-panel-title">Размерные бюджеты</div>
3966
+ <div class="micro-score">${ms.summary.godFiles + ms.summary.criticalFiles}</div>
3967
+ <div class="micro-subtle">God/critical files из ${ms.summary.sourceFiles} source-файлов. Large от ${ms.budgets.largeKb} KB или ${ms.budgets.largeLines} строк.</div>
3968
+ </div>
3969
+ <div class="micro-panel">
3970
+ <div class="micro-panel-title">Готовность модулей</div>
3971
+ <div class="micro-score" style="color:var(--green)">${ms.summary.readyModules}</div>
3972
+ <div class="micro-subtle">Ready >= 70. Risky < 40: <span style="color:${ms.summary.riskyModules ? 'var(--red)' : 'var(--text)'}">${ms.summary.riskyModules}</span>.</div>
3973
+ </div>
3974
+ <div class="micro-panel">
3975
+ <div class="micro-panel-title">Границы ownership</div>
3976
+ <div class="micro-score" style="color:${ms.summary.unmappedFiles || ms.summary.multiFeatureFiles ? 'var(--yellow)' : 'var(--green)'}">${ms.summary.unmappedFiles + ms.summary.multiFeatureFiles}</div>
3977
+ <div class="micro-subtle">Unknown ownership: ${ms.summary.unmappedFiles}. Multi-feature files: ${ms.summary.multiFeatureFiles}.</div>
3978
+ </div>
3979
+ </div>
3980
+
3981
+ <div class="micro-panel">
3982
+ <div class="micro-panel-title">Top risks</div>
3983
+ <div class="micro-risk-list">
3984
+ ${riskFiles.length ? riskFiles.map(f => `
3985
+ <div class="micro-risk-item">
3986
+ <div>
3987
+ <div class="micro-path" title="${escapeHtml(f.path)}">${escapeHtml(f.path)}</div>
3988
+ ${microRiskPills(f.risks, 4)}
3989
+ </div>
3990
+ <div style="text-align:right">
3991
+ <div style="color:${f.sizeCategory === 'critical' || f.sizeCategory === 'god' ? 'var(--red)' : 'var(--yellow)'};font-weight:700">${microCategoryLabel(f.sizeCategory)}</div>
3992
+ <div class="micro-subtle">${f.sizeKb} KB · ${f.lineCount} lines</div>
3993
+ </div>
3994
+ </div>`).join('') : `<div class="micro-subtle">Критичных рисков по текущим фильтрам нет.</div>`}
3995
+ </div>
3996
+ </div>
3997
+
3998
+ <div class="micro-section-head">
3999
+ <h3>Модули и готовность</h3>
4000
+ <span class="micro-subtle">${modules.length} из ${ms.modules.length}</span>
4001
+ </div>
4002
+ <table class="micro-table">
4003
+ <thead>
4004
+ <tr>
4005
+ <th>Модуль</th>
4006
+ <th>Score</th>
4007
+ <th>Файлы</th>
4008
+ <th>Largest file</th>
4009
+ <th>Boundary hints</th>
4010
+ <th>Top risks</th>
4011
+ </tr>
4012
+ </thead>
4013
+ <tbody>
4014
+ ${modules.map(m => `
4015
+ <tr class="clickable micro-module-row" data-micro-key="${escapeHtml(m.key)}">
4016
+ <td>
4017
+ <div style="font-weight:700;color:var(--text)">${escapeHtml(m.label)}</div>
4018
+ <div class="micro-subtle">${escapeHtml(m.key)}</div>
4019
+ </td>
4020
+ <td>
4021
+ <div style="font-weight:700;color:${microScoreColor(m.readinessScore)}">${m.readinessScore}</div>
4022
+ <span class="micro-pill ${m.readinessTier}">${microTierLabel(m.readinessTier)}</span>
4023
+ </td>
4024
+ <td>
4025
+ <div>${m.fileCount} total</div>
4026
+ <div class="micro-subtle">${m.criticalFileCount + m.godFileCount} god/critical · ${m.testedCount} tested</div>
4027
+ </td>
4028
+ <td>
4029
+ ${m.largestFile ? `<div class="micro-path" title="${escapeHtml(m.largestFile.path)}">${escapeHtml(m.largestFile.path)}</div><div class="micro-subtle">${m.largestFile.sizeKb} KB · ${m.largestFile.lineCount} lines</div>` : '<span class="micro-subtle">—</span>'}
4030
+ </td>
4031
+ <td><div class="micro-pill-row">${(m.boundaryHints || []).slice(0, 4).map(h => `<span class="micro-pill">${escapeHtml(h)}</span>`).join('') || '<span class="micro-subtle">—</span>'}</div></td>
4032
+ <td>${(m.topRisks || []).slice(0, 3).map(r => `<div class="micro-subtle">${escapeHtml(r)}</div>`).join('') || '<span class="micro-subtle">—</span>'}</td>
4033
+ </tr>`).join('')}
4034
+ </tbody>
4035
+ </table>
4036
+
4037
+ <div class="micro-section-head">
4038
+ <h3>God и large files</h3>
4039
+ <span class="micro-subtle">${problemFiles.length} файлов по фильтру</span>
4040
+ </div>
4041
+ ${renderMicroFileTable(problemFiles)}
4042
+ `;
4043
+
4044
+ c.querySelectorAll('.micro-module-row').forEach(row => {
4045
+ row.onclick = () => {
4046
+ microDrillKey = row.dataset.microKey;
4047
+ renderContent();
4048
+ };
4049
+ });
4050
+ }
4051
+
4052
+ function renderMicroFileTable(files) {
4053
+ const rows = files
4054
+ .sort((a, b) => b.sizeBytes - a.sizeBytes)
4055
+ .slice(0, 250)
4056
+ .map(f => `
4057
+ <tr>
4058
+ <td>
4059
+ <div class="micro-path" title="${escapeHtml(f.path)}">${escapeHtml(f.path)}</div>
4060
+ <div class="micro-subtle">${escapeHtml(f.type)} · deps ${f.dependencyCount}</div>
4061
+ </td>
4062
+ <td>
4063
+ <span class="micro-pill ${f.sizeCategory === 'normal' ? '' : f.sizeCategory === 'large' ? 'medium' : 'high'}">${microCategoryLabel(f.sizeCategory)}</span>
4064
+ </td>
4065
+ <td>${f.sizeKb} KB</td>
4066
+ <td>${f.lineCount}</td>
4067
+ <td>${microFeatureLabels(f.featureKeys)}</td>
4068
+ <td>${microRiskPills(f.risks, 4)}</td>
4069
+ </tr>`).join('');
4070
+ return `
4071
+ <table class="micro-table">
4072
+ <thead>
4073
+ <tr>
4074
+ <th>Файл</th>
4075
+ <th>Размер</th>
4076
+ <th>KB</th>
4077
+ <th>Строки</th>
4078
+ <th>Ownership</th>
4079
+ <th>Причины риска</th>
4080
+ </tr>
4081
+ </thead>
4082
+ <tbody>${rows || `<tr><td colspan="6"><div class="micro-subtle">Файлы не найдены по текущим фильтрам.</div></td></tr>`}</tbody>
4083
+ </table>`;
4084
+ }
4085
+
4086
+ function renderMicroserviceDetail(c, ms, key) {
4087
+ const module = (ms.modules || []).find(m => m.key === key);
4088
+ if (!module) {
4089
+ microDrillKey = null;
4090
+ renderMicroservices(c);
4091
+ return;
4092
+ }
4093
+ const files = filteredMicroFiles(ms, key).sort((a, b) => b.sizeBytes - a.sizeBytes);
4094
+ c.innerHTML = `
4095
+ <button class="micro-back" id="microBackBtn">← Назад к обзору</button>
4096
+ <div class="micro-section-head">
4097
+ <div>
4098
+ <h3>${escapeHtml(module.label)}</h3>
4099
+ <div class="micro-subtle">${escapeHtml(module.key)} · ${module.fileCount} files</div>
4100
+ </div>
4101
+ <div style="text-align:right">
4102
+ <div style="font-size:28px;font-weight:700;color:${microScoreColor(module.readinessScore)}">${module.readinessScore}</div>
4103
+ <span class="micro-pill ${module.readinessTier}">${microTierLabel(module.readinessTier)}</span>
4104
+ </div>
4105
+ </div>
4106
+
4107
+ <div class="micro-summary-grid">
4108
+ ${[
4109
+ ['Size health', module.sizeHealth],
4110
+ ['Ownership clarity', module.ownershipClarity],
4111
+ ['Test readiness', module.testReadiness],
4112
+ ['Boundary readiness', module.boundaryReadiness],
4113
+ ['Observability', module.observabilityReadiness],
4114
+ ].map(([label, value]) => `
4115
+ <div class="micro-panel">
4116
+ <div class="micro-panel-title">${label}</div>
4117
+ <div class="micro-score" style="color:${microScoreColor(value)}">${value}</div>
4118
+ <div class="micro-score-bar"><div class="micro-score-fill" style="width:${value}%;background:${microScoreColor(value)}"></div></div>
4119
+ </div>`).join('')}
4120
+ </div>
4121
+
4122
+ <div class="micro-summary-grid">
4123
+ <div class="micro-panel">
4124
+ <div class="micro-panel-title">Boundary hints</div>
4125
+ <div class="micro-pill-row">${(module.boundaryHints || []).map(h => `<span class="micro-pill">${escapeHtml(h)}</span>`).join('') || '<span class="micro-subtle">Нет явных hints</span>'}</div>
4126
+ </div>
4127
+ <div class="micro-panel">
4128
+ <div class="micro-panel-title">Service map</div>
4129
+ <div class="micro-subtle">Nodes: ${module.serviceNodes.length ? module.serviceNodes.map(escapeHtml).join(', ') : '—'}</div>
4130
+ <div class="micro-subtle">Pipelines: ${module.pipelineIds.length ? module.pipelineIds.map(escapeHtml).join(', ') : '—'}</div>
4131
+ </div>
4132
+ <div class="micro-panel">
4133
+ <div class="micro-panel-title">Tests and ownership</div>
4134
+ <div class="micro-subtle">Tested: ${module.testedCount}/${module.fileCount}</div>
4135
+ <div class="micro-subtle">Stale tests: ${module.staleTestCount}</div>
4136
+ <div class="micro-subtle">Multi-feature files: ${module.multiFeatureFileCount}</div>
4137
+ </div>
4138
+ </div>
4139
+
4140
+ <div class="micro-section-head">
4141
+ <h3>Файлы модуля</h3>
4142
+ <span class="micro-subtle">${files.length} по текущим фильтрам</span>
4143
+ </div>
4144
+ ${renderMicroFileTable(files)}
4145
+ `;
4146
+ document.getElementById('microBackBtn').onclick = () => {
4147
+ microDrillKey = null;
4148
+ renderContent();
4149
+ };
4150
+ }
4151
+
4152
+ // ─── Content ──────────────────────────────────────────────────────────────────
3697
4153
  function renderContent() {
3698
4154
  const c = document.getElementById('content');
3699
- if (contextMode === 'observability') {
3700
- renderObservability(c);
3701
- return;
3702
- }
3703
- if (contextMode === 'docs') {
4155
+ if (contextMode === 'observability') {
4156
+ renderObservability(c);
4157
+ return;
4158
+ }
4159
+ if (contextMode === 'microservices') {
4160
+ renderMicroservices(c);
4161
+ return;
4162
+ }
4163
+ if (contextMode === 'docs') {
3704
4164
  renderDocumentation(c);
3705
4165
  return;
3706
4166
  }
@@ -8503,8 +8963,11 @@ function getLoadPromptAggregate(summary) {
8503
8963
  const values = endpoints.map(endpoint => numberOrNull(endpoint[field])).filter(v => v != null);
8504
8964
  return values.length ? Math.max(...values) : null;
8505
8965
  };
8966
+ const totalRequests = numberOrNull(summary?.totalRequests)
8967
+ ?? numberOrNull(loadState?.totalRequests)
8968
+ ?? (totalFromEndpoints || null);
8506
8969
  return {
8507
- totalRequests: numberOrNull(summary?.totalRequests) ?? numberOrNull(loadState?.totalRequests) ?? totalFromEndpoints || null,
8970
+ totalRequests,
8508
8971
  rps: numberOrNull(summary?.rps),
8509
8972
  avgDuration: numberOrNull(summary?.avgDuration) ?? weightedAvg,
8510
8973
  p90Duration: numberOrNull(summary?.p90Duration) ?? maxEndpoint('p90Duration'),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.230",
3
+ "version": "0.3.232",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {