viberadar 0.3.81 → 0.3.83

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.
@@ -389,6 +389,78 @@
389
389
  .feature-progress-fill { height: 100%; border-radius: 3px; transition: width 0.4s; }
390
390
  .feature-progress-label { font-size: 11px; color: var(--muted); white-space: nowrap; }
391
391
 
392
+ /* ── Documentation screen ────────────────────────────────────────────────── */
393
+ .doc-kpi-strip {
394
+ display: flex; gap: 24px; padding: 12px 16px; margin-bottom: 16px;
395
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
396
+ }
397
+ .doc-kpi { display: flex; flex-direction: column; align-items: center; gap: 2px; }
398
+ .doc-kpi-val { font-size: 20px; font-weight: 700; color: var(--text); }
399
+ .doc-kpi-lbl { font-size: 11px; color: var(--muted); }
400
+ .doc-feature-card { cursor: pointer; }
401
+ .doc-agent-btn {
402
+ font-size: 11px; padding: 4px 10px; border-radius: 6px;
403
+ border: 1px solid var(--border); background: var(--bg-hover); color: var(--text);
404
+ cursor: pointer; transition: background 0.15s;
405
+ }
406
+ .doc-agent-btn:hover { background: var(--blue); color: #fff; border-color: var(--blue); }
407
+ .doc-agent-btn-secondary { opacity: 0.75; }
408
+ .doc-agent-btn-secondary:hover { opacity: 1; }
409
+ .doc-detail-actions {
410
+ display: flex; gap: 12px; margin-bottom: 16px; flex-wrap: wrap;
411
+ }
412
+ .doc-action-step {
413
+ display: flex; gap: 10px; align-items: flex-start;
414
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
415
+ padding: 12px 14px; flex: 1; min-width: 220px;
416
+ }
417
+ .doc-step-num {
418
+ display: flex; align-items: center; justify-content: center;
419
+ width: 22px; height: 22px; border-radius: 50%; background: var(--dim);
420
+ color: var(--text); font-size: 11px; font-weight: 700; flex-shrink: 0; margin-top: 2px;
421
+ }
422
+ .doc-step-label { font-size: 13px; font-weight: 600; color: var(--text); margin-bottom: 3px; }
423
+ .doc-step-hint { font-size: 11px; color: var(--muted); margin-bottom: 8px; line-height: 1.4; }
424
+ .doc-changed-section {
425
+ margin-bottom: 16px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
426
+ }
427
+ .doc-changed-header {
428
+ padding: 8px 12px; background: var(--bg-card); color: var(--text);
429
+ font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 8px;
430
+ }
431
+ .doc-changed-arrow { font-size: 10px; transition: transform 0.2s; }
432
+ .doc-changed-section.open .doc-changed-arrow { transform: rotate(90deg); }
433
+ .doc-changed-list { display: none; padding: 8px 12px; }
434
+ .doc-changed-section.open .doc-changed-list { display: block; }
435
+ .doc-changed-file {
436
+ font-size: 12px; color: var(--yellow); padding: 3px 0;
437
+ font-family: 'SF Mono', 'Fira Code', monospace;
438
+ }
439
+ .doc-content {
440
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
441
+ padding: 20px 24px; line-height: 1.65; color: var(--text);
442
+ }
443
+ .doc-content .doc-h { margin: 20px 0 8px 0; color: var(--text); font-weight: 600; }
444
+ .doc-content h1.doc-h { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
445
+ .doc-content h2.doc-h { font-size: 17px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
446
+ .doc-content h3.doc-h { font-size: 15px; }
447
+ .doc-content .doc-p { margin: 8px 0; font-size: 14px; }
448
+ .doc-content .doc-list { margin: 8px 0; padding-left: 24px; font-size: 14px; }
449
+ .doc-content .doc-list li { margin: 4px 0; }
450
+ .doc-content .doc-code {
451
+ background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
452
+ padding: 12px 16px; overflow-x: auto; font-size: 13px; margin: 12px 0;
453
+ }
454
+ .doc-content .doc-inline-code {
455
+ background: var(--bg); border: 1px solid var(--border); border-radius: 3px;
456
+ padding: 1px 5px; font-size: 12px;
457
+ }
458
+ .back-btn {
459
+ background: none; border: 1px solid var(--border); color: var(--muted);
460
+ padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px;
461
+ }
462
+ .back-btn:hover { color: var(--text); border-color: var(--dim); }
463
+
392
464
  /* ── No config banner ────────────────────────────────────────────────────── */
393
465
  .no-config {
394
466
  display: flex;
@@ -1363,15 +1435,19 @@ function switchObsTab(tabId) {
1363
1435
  const modeStore = {
1364
1436
  qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
1365
1437
  observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
1438
+ docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
1366
1439
  };
1367
1440
 
1368
1441
  function getModeFromPath(pathname = window.location.pathname) {
1369
1442
  if (pathname.startsWith('/radar/observability')) return 'observability';
1443
+ if (pathname.startsWith('/radar/docs')) return 'docs';
1370
1444
  return 'qa';
1371
1445
  }
1372
1446
 
1373
1447
  function routePathForMode(mode) {
1374
- return mode === 'observability' ? '/radar/observability' : '/radar/qa';
1448
+ if (mode === 'observability') return '/radar/observability';
1449
+ if (mode === 'docs') return '/radar/docs';
1450
+ return '/radar/qa';
1375
1451
  }
1376
1452
 
1377
1453
  function setModeRoute(mode, replace = false) {
@@ -1408,8 +1484,8 @@ function switchMode(nextMode) {
1408
1484
  saveModeState(contextMode);
1409
1485
  contextMode = nextMode;
1410
1486
  restoreModeState(contextMode);
1411
- if (contextMode === 'observability') {
1412
- view = 'files';
1487
+ if (contextMode === 'observability' || contextMode === 'docs') {
1488
+ view = 'features';
1413
1489
  drillFeatureKey = null;
1414
1490
  drillTestType = null;
1415
1491
  activePanelKey = null;
@@ -2618,6 +2694,7 @@ function renderModeSwitch() {
2618
2694
  const modes = [
2619
2695
  { key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
2620
2696
  { key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
2697
+ { key: 'docs', label: 'Документация', hint: 'Актуальность, генерация, обновление' },
2621
2698
  ];
2622
2699
  root.innerHTML = modes.map(m => `
2623
2700
  <button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
@@ -2646,6 +2723,16 @@ function renderSidebar() {
2646
2723
  return;
2647
2724
  }
2648
2725
 
2726
+ if (contextMode === 'docs') {
2727
+ tabs.style.display = 'none';
2728
+ extra.innerHTML = `
2729
+ <div class="sidebar-label">Документация</div>
2730
+ <div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
2731
+ Статус и актуальность документации по фичам.
2732
+ </div>`;
2733
+ return;
2734
+ }
2735
+
2649
2736
  tabs.style.display = 'flex';
2650
2737
  document.querySelectorAll('.view-tab').forEach(t =>
2651
2738
  t.classList.toggle('active', t.dataset.view === view)
@@ -2690,6 +2777,10 @@ function renderContent() {
2690
2777
  renderObservability(c);
2691
2778
  return;
2692
2779
  }
2780
+ if (contextMode === 'docs') {
2781
+ renderDocumentation(c);
2782
+ return;
2783
+ }
2693
2784
 
2694
2785
  if (view === 'features') {
2695
2786
  if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
@@ -3531,6 +3622,283 @@ function renderObservabilityOverview(c) {
3531
3622
  </div>`;
3532
3623
  }
3533
3624
 
3625
+ // ─── Documentation screen ─────────────────────────────────────────────────────
3626
+
3627
+ function renderDocumentation(c) {
3628
+ const doc = D.documentation;
3629
+ if (!doc) {
3630
+ c.innerHTML = `<div class="onboarding-block"><h3>Документация</h3><p>Данные документации недоступны. Убедитесь, что в проекте есть <code>viberadar.config.json</code> с фичами и пересканируйте проект.</p></div>`;
3631
+ return;
3632
+ }
3633
+ if (drillFeatureKey) renderDocFeatureDetail(c);
3634
+ else renderDocFeatureCards(c);
3635
+ }
3636
+
3637
+ function renderDocFeatureCards(c) {
3638
+ const doc = D.documentation;
3639
+ if (!doc || !doc.features.length) {
3640
+ c.innerHTML = `<div class="onboarding-block"><h3>Документация</h3><p>Фичи не найдены. Добавьте фичи в viberadar.config.json.</p></div>`;
3641
+ return;
3642
+ }
3643
+
3644
+ const q = searchQuery.toLowerCase();
3645
+ const filtered = doc.features.filter(f => !q || f.label.toLowerCase().includes(q) || f.key.toLowerCase().includes(q));
3646
+
3647
+ const docPct = doc.totalFeatures > 0 ? Math.round(((doc.freshCount + doc.staleCount) / doc.totalFeatures) * 100) : 0;
3648
+
3649
+ let html = `
3650
+ <div class="doc-kpi-strip">
3651
+ <div class="doc-kpi"><span class="doc-kpi-val">${doc.totalFeatures}</span><span class="doc-kpi-lbl">Всего фич</span></div>
3652
+ <div class="doc-kpi"><span class="doc-kpi-val" style="color:var(--green)">${doc.freshCount}</span><span class="doc-kpi-lbl">Актуальных</span></div>
3653
+ <div class="doc-kpi"><span class="doc-kpi-val" style="color:var(--yellow)">${doc.staleCount}</span><span class="doc-kpi-lbl">Устаревших</span></div>
3654
+ <div class="doc-kpi"><span class="doc-kpi-val" style="color:var(--red)">${doc.missingCount}</span><span class="doc-kpi-lbl">Без доки</span></div>
3655
+ <div class="doc-kpi"><span class="doc-kpi-val">${docPct}%</span><span class="doc-kpi-lbl">Задокументировано</span></div>
3656
+ </div>
3657
+ <div class="features-grid">`;
3658
+
3659
+ for (const f of filtered) {
3660
+ const statusColor = !f.docExists ? 'var(--red)' : f.isStale ? 'var(--yellow)' : 'var(--green)';
3661
+ const statusText = !f.docExists ? 'Отсутствует' : f.isStale ? `Устарела (${f.changedFilesSinceDoc.length} файл.)` : 'Актуальна';
3662
+ const statusDot = `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${statusColor};margin-right:6px"></span>`;
3663
+ const lastUpd = f.lastUpdated ? new Date(f.lastUpdated).toLocaleDateString('ru-RU') : '—';
3664
+ const freshFiles = f.docExists ? (f.sourceFileCount - f.changedFilesSinceDoc.length) : 0;
3665
+ const progressPct = f.sourceFileCount > 0 ? Math.round((freshFiles / f.sourceFileCount) * 100) : 0;
3666
+ const progressColor = !f.docExists ? 'var(--red)' : f.isStale ? 'var(--yellow)' : 'var(--green)';
3667
+
3668
+ const syncBtn = `<button class="agent-card-btn doc-agent-btn" data-task="sync-docs-structure" data-key="${f.key}" style="margin-top:8px" title="Создать скелет / обновить список файлов">Актуализировать структуру</button>`;
3669
+ const fillBtn = f.docExists
3670
+ ? `<button class="agent-card-btn doc-agent-btn doc-agent-btn-secondary" data-task="${f.isStale ? 'update-docs' : 'generate-docs'}" data-key="${f.key}" style="margin-top:4px">${f.isStale ? 'Обновить содержимое' : 'Заполнить содержимое'}</button>`
3671
+ : '';
3672
+ const agentBtn = (f.docExists && !f.isStale)
3673
+ ? `<div style="margin-top:8px;font-size:11px;color:var(--green)">Документация актуальна</div>${syncBtn}`
3674
+ : `<div style="display:flex;flex-direction:column">${syncBtn}${fillBtn}</div>`;
3675
+
3676
+ html += `
3677
+ <div class="feature-card doc-feature-card" data-key="${f.key}">
3678
+ <div class="feature-accent" style="background:${f.color}"></div>
3679
+ <div class="feature-body">
3680
+ <div class="feature-title">
3681
+ <span>${escapeHtml(f.label)}</span>
3682
+ <span class="feature-file-count">${f.sourceFileCount} файл.</span>
3683
+ </div>
3684
+ <div style="display:flex;align-items:center;gap:4px;margin:6px 0;font-size:12px">
3685
+ ${statusDot}<span style="color:${statusColor};font-weight:600">${statusText}</span>
3686
+ </div>
3687
+ <div style="font-size:11px;color:var(--muted);margin-bottom:6px">Обновлено: ${lastUpd}</div>
3688
+ <div class="feature-progress-bar">
3689
+ <div class="feature-progress-fill" style="width:${f.docExists ? progressPct : 0}%;background:${progressColor}"></div>
3690
+ </div>
3691
+ <div class="feature-progress-label">${f.docExists ? (f.isStale ? `${f.changedFilesSinceDoc.length} из ${f.sourceFileCount} изменены` : `${f.sourceFileCount} из ${f.sourceFileCount} актуальны`) : 'Документация отсутствует'}</div>
3692
+ ${agentBtn}
3693
+ </div>
3694
+ </div>`;
3695
+ }
3696
+ html += '</div>';
3697
+ c.innerHTML = html;
3698
+
3699
+ // Card click → drill-down
3700
+ c.querySelectorAll('.doc-feature-card').forEach(card => {
3701
+ card.addEventListener('click', (e) => {
3702
+ if (e.target.closest('.doc-agent-btn')) return;
3703
+ setFeatureDrill(card.dataset.key);
3704
+ });
3705
+ });
3706
+
3707
+ // Agent button clicks
3708
+ c.querySelectorAll('.doc-agent-btn').forEach(btn => {
3709
+ btn.addEventListener('click', (e) => {
3710
+ e.stopPropagation();
3711
+ runAgentTask(btn.dataset.task, btn.dataset.key);
3712
+ });
3713
+ });
3714
+ }
3715
+
3716
+ // Lightweight markdown → HTML renderer
3717
+ function renderMarkdownToHtml(md) {
3718
+ let html = '';
3719
+ const lines = md.split('\n');
3720
+ let inCodeBlock = false;
3721
+ let codeBlockContent = '';
3722
+ let inList = false;
3723
+ let listType = '';
3724
+
3725
+ for (let i = 0; i < lines.length; i++) {
3726
+ const line = lines[i];
3727
+
3728
+ // Code blocks
3729
+ if (line.trimStart().startsWith('```')) {
3730
+ if (inCodeBlock) {
3731
+ html += `<pre class="doc-code"><code>${escapeHtml(codeBlockContent.trimEnd())}</code></pre>`;
3732
+ codeBlockContent = '';
3733
+ inCodeBlock = false;
3734
+ } else {
3735
+ if (inList) { html += listType === 'ul' ? '</ul>' : '</ol>'; inList = false; }
3736
+ inCodeBlock = true;
3737
+ }
3738
+ continue;
3739
+ }
3740
+ if (inCodeBlock) {
3741
+ codeBlockContent += line + '\n';
3742
+ continue;
3743
+ }
3744
+
3745
+ // Headings
3746
+ const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
3747
+ if (headingMatch) {
3748
+ if (inList) { html += listType === 'ul' ? '</ul>' : '</ol>'; inList = false; }
3749
+ const level = headingMatch[1].length;
3750
+ html += `<h${level} class="doc-h">${inlineFormat(headingMatch[2])}</h${level}>`;
3751
+ continue;
3752
+ }
3753
+
3754
+ // Unordered list
3755
+ if (/^\s*[-*]\s+/.test(line)) {
3756
+ if (!inList || listType !== 'ul') {
3757
+ if (inList) html += listType === 'ul' ? '</ul>' : '</ol>';
3758
+ html += '<ul class="doc-list">';
3759
+ inList = true;
3760
+ listType = 'ul';
3761
+ }
3762
+ html += `<li>${inlineFormat(line.replace(/^\s*[-*]\s+/, ''))}</li>`;
3763
+ continue;
3764
+ }
3765
+
3766
+ // Ordered list
3767
+ if (/^\s*\d+\.\s+/.test(line)) {
3768
+ if (!inList || listType !== 'ol') {
3769
+ if (inList) html += listType === 'ul' ? '</ul>' : '</ol>';
3770
+ html += '<ol class="doc-list">';
3771
+ inList = true;
3772
+ listType = 'ol';
3773
+ }
3774
+ html += `<li>${inlineFormat(line.replace(/^\s*\d+\.\s+/, ''))}</li>`;
3775
+ continue;
3776
+ }
3777
+
3778
+ // End list
3779
+ if (inList && line.trim() === '') {
3780
+ html += listType === 'ul' ? '</ul>' : '</ol>';
3781
+ inList = false;
3782
+ }
3783
+
3784
+ // Empty line
3785
+ if (line.trim() === '') continue;
3786
+
3787
+ // Paragraph
3788
+ if (!inList) {
3789
+ html += `<p class="doc-p">${inlineFormat(line)}</p>`;
3790
+ }
3791
+ }
3792
+
3793
+ if (inCodeBlock) {
3794
+ html += `<pre class="doc-code"><code>${escapeHtml(codeBlockContent.trimEnd())}</code></pre>`;
3795
+ }
3796
+ if (inList) html += listType === 'ul' ? '</ul>' : '</ol>';
3797
+ return html;
3798
+ }
3799
+
3800
+ function inlineFormat(text) {
3801
+ let s = escapeHtml(text);
3802
+ s = s.replace(/`([^`]+)`/g, '<code class="doc-inline-code">$1</code>');
3803
+ s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
3804
+ s = s.replace(/\*([^*]+)\*/g, '<em>$1</em>');
3805
+ s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--blue)">$1</a>');
3806
+ return s;
3807
+ }
3808
+
3809
+ let docContentCache = {};
3810
+
3811
+ async function renderDocFeatureDetail(c) {
3812
+ const doc = D.documentation;
3813
+ const f = doc?.features.find(x => x.key === drillFeatureKey);
3814
+ if (!f) {
3815
+ c.innerHTML = `<div class="onboarding-block"><p>Фича не найдена.</p></div>`;
3816
+ return;
3817
+ }
3818
+
3819
+ const statusColor = !f.docExists ? 'var(--red)' : f.isStale ? 'var(--yellow)' : 'var(--green)';
3820
+ const statusText = !f.docExists ? 'Отсутствует' : f.isStale ? `Устарела (${f.changedFilesSinceDoc.length} файл.)` : 'Актуальна';
3821
+ const statusDot = `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${statusColor};margin-right:6px"></span>`;
3822
+ const lastUpd = f.lastUpdated ? new Date(f.lastUpdated).toLocaleDateString('ru-RU') : '—';
3823
+
3824
+ // Two-step action buttons
3825
+ const detailSyncBtn = `<button class="agent-card-btn doc-detail-agent-btn" data-task="sync-docs-structure" data-key="${f.key}" title="Создать скелет / обновить список файлов фичи">Актуализировать структуру</button>`;
3826
+ const detailFillTask = f.isStale ? 'update-docs' : 'generate-docs';
3827
+ const detailFillLabel = f.isStale
3828
+ ? `Обновить содержимое (${f.changedFilesSinceDoc.length} изм. файл.)`
3829
+ : 'Заполнить содержимое';
3830
+ const detailFillBtn = `<button class="agent-card-btn doc-detail-agent-btn doc-fill-btn" data-task="${detailFillTask}" data-key="${f.key}">${detailFillLabel}</button>`;
3831
+ const detailActionsHtml = `
3832
+ <div class="doc-detail-actions">
3833
+ <div class="doc-action-step">
3834
+ <span class="doc-step-num">1</span>
3835
+ <div>
3836
+ <div class="doc-step-label">Актуализировать структуру</div>
3837
+ <div class="doc-step-hint">Создаёт скелет / обновляет список файлов. Не читает содержимое.</div>
3838
+ ${detailSyncBtn}
3839
+ </div>
3840
+ </div>
3841
+ <div class="doc-action-step">
3842
+ <span class="doc-step-num">2</span>
3843
+ <div>
3844
+ <div class="doc-step-label">${f.isStale ? 'Обновить содержимое' : 'Заполнить содержимое'}</div>
3845
+ <div class="doc-step-hint">${f.isStale ? 'Агент читает изменившиеся файлы и обновляет доку.' : 'Агент читает файлы и заполняет все секции.'}</div>
3846
+ ${detailFillBtn}
3847
+ </div>
3848
+ </div>
3849
+ </div>`;
3850
+
3851
+ let changedFilesHtml = '';
3852
+ if (f.isStale && f.changedFilesSinceDoc.length > 0) {
3853
+ changedFilesHtml = `
3854
+ <div class="doc-changed-section">
3855
+ <div class="doc-changed-header" onclick="this.parentElement.classList.toggle('open')">
3856
+ Изменённые файлы (${f.changedFilesSinceDoc.length})
3857
+ <span class="doc-changed-arrow">&#9654;</span>
3858
+ </div>
3859
+ <div class="doc-changed-list">
3860
+ ${f.changedFilesSinceDoc.map(fp => `<div class="doc-changed-file">${escapeHtml(fp)}</div>`).join('')}
3861
+ </div>
3862
+ </div>`;
3863
+ }
3864
+
3865
+ let docContentHtml = '';
3866
+ if (f.docExists) {
3867
+ // Fetch content if not cached
3868
+ if (!docContentCache[f.key]) {
3869
+ try {
3870
+ const resp = await fetch('/api/docs/content?feature=' + encodeURIComponent(f.key));
3871
+ const data = await resp.json();
3872
+ if (data.exists && data.content) docContentCache[f.key] = data.content;
3873
+ } catch {}
3874
+ }
3875
+ const raw = docContentCache[f.key] || '';
3876
+ docContentHtml = raw ? `<div class="doc-content">${renderMarkdownToHtml(raw)}</div>` : '<div style="color:var(--muted);padding:16px">Не удалось загрузить содержимое документации.</div>';
3877
+ } else {
3878
+ docContentHtml = `<div style="color:var(--muted);padding:16px;text-align:center">Документация ещё не создана. Нажмите кнопку ниже, чтобы сгенерировать.</div>`;
3879
+ }
3880
+
3881
+ c.innerHTML = `
3882
+ <div style="margin-bottom:16px">
3883
+ <button class="back-btn" id="docBackBtn">&#8592; Все фичи</button>
3884
+ </div>
3885
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:16px">
3886
+ <div style="width:4px;height:28px;border-radius:2px;background:${f.color}"></div>
3887
+ <h2 style="margin:0;font-size:18px;color:var(--text)">${escapeHtml(f.label)}</h2>
3888
+ <div style="display:flex;align-items:center;font-size:13px">${statusDot}<span style="color:${statusColor};font-weight:600">${statusText}</span></div>
3889
+ <span style="font-size:12px;color:var(--muted)">Обновлено: ${lastUpd}</span>
3890
+ </div>
3891
+ ${detailActionsHtml}
3892
+ ${changedFilesHtml}
3893
+ ${docContentHtml}`;
3894
+
3895
+ document.getElementById('docBackBtn').onclick = () => backToFeatures();
3896
+
3897
+ c.querySelectorAll('.doc-detail-agent-btn').forEach(btn => {
3898
+ btn.addEventListener('click', () => runAgentTask(btn.dataset.task, btn.dataset.key));
3899
+ });
3900
+ }
3901
+
3534
3902
  function renderFeatureCards(c) {
3535
3903
  if (!D.hasConfig || !D.features) {
3536
3904
  c.innerHTML = `
@@ -4637,6 +5005,7 @@ async function refreshData() {
4637
5005
  try {
4638
5006
  const res = await fetch('/api/data');
4639
5007
  D = await res.json();
5008
+ docContentCache = {};
4640
5009
 
4641
5010
  // Update header timestamp
4642
5011
  document.getElementById('scannedAt').textContent =
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "viberadar",
3
- "version": "0.3.81",
3
+ "version": "0.3.83",
4
4
  "description": "Live module map with test coverage for vibecoding projects",
5
5
  "main": "./dist/cli.js",
6
6
  "bin": {