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.
- package/dist/scanner/index.d.ts +23 -0
- package/dist/scanner/index.d.ts.map +1 -1
- package/dist/scanner/index.js +76 -0
- package/dist/scanner/index.js.map +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +224 -1
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +372 -3
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -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
|
-
|
|
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 = '
|
|
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">▶</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">← Все фичи</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 =
|