viberadar 0.3.81 → 0.3.82
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 +143 -1
- package/dist/server/index.js.map +1 -1
- package/dist/ui/dashboard.html +330 -3
- package/package.json +1 -1
package/dist/ui/dashboard.html
CHANGED
|
@@ -389,6 +389,61 @@
|
|
|
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-changed-section {
|
|
408
|
+
margin-bottom: 16px; border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
|
|
409
|
+
}
|
|
410
|
+
.doc-changed-header {
|
|
411
|
+
padding: 8px 12px; background: var(--bg-card); color: var(--text);
|
|
412
|
+
font-size: 13px; font-weight: 600; cursor: pointer; display: flex; align-items: center; gap: 8px;
|
|
413
|
+
}
|
|
414
|
+
.doc-changed-arrow { font-size: 10px; transition: transform 0.2s; }
|
|
415
|
+
.doc-changed-section.open .doc-changed-arrow { transform: rotate(90deg); }
|
|
416
|
+
.doc-changed-list { display: none; padding: 8px 12px; }
|
|
417
|
+
.doc-changed-section.open .doc-changed-list { display: block; }
|
|
418
|
+
.doc-changed-file {
|
|
419
|
+
font-size: 12px; color: var(--yellow); padding: 3px 0;
|
|
420
|
+
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
421
|
+
}
|
|
422
|
+
.doc-content {
|
|
423
|
+
background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px;
|
|
424
|
+
padding: 20px 24px; line-height: 1.65; color: var(--text);
|
|
425
|
+
}
|
|
426
|
+
.doc-content .doc-h { margin: 20px 0 8px 0; color: var(--text); font-weight: 600; }
|
|
427
|
+
.doc-content h1.doc-h { font-size: 22px; border-bottom: 1px solid var(--border); padding-bottom: 8px; }
|
|
428
|
+
.doc-content h2.doc-h { font-size: 17px; border-bottom: 1px solid var(--border); padding-bottom: 6px; }
|
|
429
|
+
.doc-content h3.doc-h { font-size: 15px; }
|
|
430
|
+
.doc-content .doc-p { margin: 8px 0; font-size: 14px; }
|
|
431
|
+
.doc-content .doc-list { margin: 8px 0; padding-left: 24px; font-size: 14px; }
|
|
432
|
+
.doc-content .doc-list li { margin: 4px 0; }
|
|
433
|
+
.doc-content .doc-code {
|
|
434
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 6px;
|
|
435
|
+
padding: 12px 16px; overflow-x: auto; font-size: 13px; margin: 12px 0;
|
|
436
|
+
}
|
|
437
|
+
.doc-content .doc-inline-code {
|
|
438
|
+
background: var(--bg); border: 1px solid var(--border); border-radius: 3px;
|
|
439
|
+
padding: 1px 5px; font-size: 12px;
|
|
440
|
+
}
|
|
441
|
+
.back-btn {
|
|
442
|
+
background: none; border: 1px solid var(--border); color: var(--muted);
|
|
443
|
+
padding: 4px 12px; border-radius: 6px; cursor: pointer; font-size: 13px;
|
|
444
|
+
}
|
|
445
|
+
.back-btn:hover { color: var(--text); border-color: var(--dim); }
|
|
446
|
+
|
|
392
447
|
/* ── No config banner ────────────────────────────────────────────────────── */
|
|
393
448
|
.no-config {
|
|
394
449
|
display: flex;
|
|
@@ -1363,15 +1418,19 @@ function switchObsTab(tabId) {
|
|
|
1363
1418
|
const modeStore = {
|
|
1364
1419
|
qa: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1365
1420
|
observability: { view: 'files', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1421
|
+
docs: { view: 'features', searchQuery: '', activeTypes: new Set(), drillFeatureKey: null, drillTestType: null, activePanelKey: null, showOnlyUntestedInFeature: false },
|
|
1366
1422
|
};
|
|
1367
1423
|
|
|
1368
1424
|
function getModeFromPath(pathname = window.location.pathname) {
|
|
1369
1425
|
if (pathname.startsWith('/radar/observability')) return 'observability';
|
|
1426
|
+
if (pathname.startsWith('/radar/docs')) return 'docs';
|
|
1370
1427
|
return 'qa';
|
|
1371
1428
|
}
|
|
1372
1429
|
|
|
1373
1430
|
function routePathForMode(mode) {
|
|
1374
|
-
|
|
1431
|
+
if (mode === 'observability') return '/radar/observability';
|
|
1432
|
+
if (mode === 'docs') return '/radar/docs';
|
|
1433
|
+
return '/radar/qa';
|
|
1375
1434
|
}
|
|
1376
1435
|
|
|
1377
1436
|
function setModeRoute(mode, replace = false) {
|
|
@@ -1408,8 +1467,8 @@ function switchMode(nextMode) {
|
|
|
1408
1467
|
saveModeState(contextMode);
|
|
1409
1468
|
contextMode = nextMode;
|
|
1410
1469
|
restoreModeState(contextMode);
|
|
1411
|
-
if (contextMode === 'observability') {
|
|
1412
|
-
view = '
|
|
1470
|
+
if (contextMode === 'observability' || contextMode === 'docs') {
|
|
1471
|
+
view = 'features';
|
|
1413
1472
|
drillFeatureKey = null;
|
|
1414
1473
|
drillTestType = null;
|
|
1415
1474
|
activePanelKey = null;
|
|
@@ -2618,6 +2677,7 @@ function renderModeSwitch() {
|
|
|
2618
2677
|
const modes = [
|
|
2619
2678
|
{ key: 'qa', label: 'QA Coverage', hint: 'Покрытие, пробелы, тренды' },
|
|
2620
2679
|
{ key: 'observability', label: 'Наблюдаемость', hint: 'Логи, шум, сигналы ошибок' },
|
|
2680
|
+
{ key: 'docs', label: 'Документация', hint: 'Актуальность, генерация, обновление' },
|
|
2621
2681
|
];
|
|
2622
2682
|
root.innerHTML = modes.map(m => `
|
|
2623
2683
|
<button class="mode-switch-btn ${contextMode === m.key ? 'active' : ''}" data-mode="${m.key}">
|
|
@@ -2646,6 +2706,16 @@ function renderSidebar() {
|
|
|
2646
2706
|
return;
|
|
2647
2707
|
}
|
|
2648
2708
|
|
|
2709
|
+
if (contextMode === 'docs') {
|
|
2710
|
+
tabs.style.display = 'none';
|
|
2711
|
+
extra.innerHTML = `
|
|
2712
|
+
<div class="sidebar-label">Документация</div>
|
|
2713
|
+
<div style="font-size:12px;color:var(--muted);padding:0 6px;line-height:1.45">
|
|
2714
|
+
Статус и актуальность документации по фичам.
|
|
2715
|
+
</div>`;
|
|
2716
|
+
return;
|
|
2717
|
+
}
|
|
2718
|
+
|
|
2649
2719
|
tabs.style.display = 'flex';
|
|
2650
2720
|
document.querySelectorAll('.view-tab').forEach(t =>
|
|
2651
2721
|
t.classList.toggle('active', t.dataset.view === view)
|
|
@@ -2690,6 +2760,10 @@ function renderContent() {
|
|
|
2690
2760
|
renderObservability(c);
|
|
2691
2761
|
return;
|
|
2692
2762
|
}
|
|
2763
|
+
if (contextMode === 'docs') {
|
|
2764
|
+
renderDocumentation(c);
|
|
2765
|
+
return;
|
|
2766
|
+
}
|
|
2693
2767
|
|
|
2694
2768
|
if (view === 'features') {
|
|
2695
2769
|
if (drillFeatureKey === '__unmapped__') renderUnmappedDetail(c);
|
|
@@ -3531,6 +3605,258 @@ function renderObservabilityOverview(c) {
|
|
|
3531
3605
|
</div>`;
|
|
3532
3606
|
}
|
|
3533
3607
|
|
|
3608
|
+
// ─── Documentation screen ─────────────────────────────────────────────────────
|
|
3609
|
+
|
|
3610
|
+
function renderDocumentation(c) {
|
|
3611
|
+
const doc = D.documentation;
|
|
3612
|
+
if (!doc) {
|
|
3613
|
+
c.innerHTML = `<div class="onboarding-block"><h3>Документация</h3><p>Данные документации недоступны. Убедитесь, что в проекте есть <code>viberadar.config.json</code> с фичами и пересканируйте проект.</p></div>`;
|
|
3614
|
+
return;
|
|
3615
|
+
}
|
|
3616
|
+
if (drillFeatureKey) renderDocFeatureDetail(c);
|
|
3617
|
+
else renderDocFeatureCards(c);
|
|
3618
|
+
}
|
|
3619
|
+
|
|
3620
|
+
function renderDocFeatureCards(c) {
|
|
3621
|
+
const doc = D.documentation;
|
|
3622
|
+
if (!doc || !doc.features.length) {
|
|
3623
|
+
c.innerHTML = `<div class="onboarding-block"><h3>Документация</h3><p>Фичи не найдены. Добавьте фичи в viberadar.config.json.</p></div>`;
|
|
3624
|
+
return;
|
|
3625
|
+
}
|
|
3626
|
+
|
|
3627
|
+
const q = searchQuery.toLowerCase();
|
|
3628
|
+
const filtered = doc.features.filter(f => !q || f.label.toLowerCase().includes(q) || f.key.toLowerCase().includes(q));
|
|
3629
|
+
|
|
3630
|
+
const docPct = doc.totalFeatures > 0 ? Math.round(((doc.freshCount + doc.staleCount) / doc.totalFeatures) * 100) : 0;
|
|
3631
|
+
|
|
3632
|
+
let html = `
|
|
3633
|
+
<div class="doc-kpi-strip">
|
|
3634
|
+
<div class="doc-kpi"><span class="doc-kpi-val">${doc.totalFeatures}</span><span class="doc-kpi-lbl">Всего фич</span></div>
|
|
3635
|
+
<div class="doc-kpi"><span class="doc-kpi-val" style="color:var(--green)">${doc.freshCount}</span><span class="doc-kpi-lbl">Актуальных</span></div>
|
|
3636
|
+
<div class="doc-kpi"><span class="doc-kpi-val" style="color:var(--yellow)">${doc.staleCount}</span><span class="doc-kpi-lbl">Устаревших</span></div>
|
|
3637
|
+
<div class="doc-kpi"><span class="doc-kpi-val" style="color:var(--red)">${doc.missingCount}</span><span class="doc-kpi-lbl">Без доки</span></div>
|
|
3638
|
+
<div class="doc-kpi"><span class="doc-kpi-val">${docPct}%</span><span class="doc-kpi-lbl">Задокументировано</span></div>
|
|
3639
|
+
</div>
|
|
3640
|
+
<div class="features-grid">`;
|
|
3641
|
+
|
|
3642
|
+
for (const f of filtered) {
|
|
3643
|
+
const statusColor = !f.docExists ? 'var(--red)' : f.isStale ? 'var(--yellow)' : 'var(--green)';
|
|
3644
|
+
const statusText = !f.docExists ? 'Отсутствует' : f.isStale ? `Устарела (${f.changedFilesSinceDoc.length} файл.)` : 'Актуальна';
|
|
3645
|
+
const statusDot = `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${statusColor};margin-right:6px"></span>`;
|
|
3646
|
+
const lastUpd = f.lastUpdated ? new Date(f.lastUpdated).toLocaleDateString('ru-RU') : '—';
|
|
3647
|
+
const freshFiles = f.docExists ? (f.sourceFileCount - f.changedFilesSinceDoc.length) : 0;
|
|
3648
|
+
const progressPct = f.sourceFileCount > 0 ? Math.round((freshFiles / f.sourceFileCount) * 100) : 0;
|
|
3649
|
+
const progressColor = !f.docExists ? 'var(--red)' : f.isStale ? 'var(--yellow)' : 'var(--green)';
|
|
3650
|
+
|
|
3651
|
+
const agentTask = !f.docExists ? 'generate-docs' : f.isStale ? 'update-docs' : null;
|
|
3652
|
+
const agentLabel = !f.docExists ? 'Написать доку' : f.isStale ? 'Обновить доку' : '';
|
|
3653
|
+
const agentBtn = agentTask
|
|
3654
|
+
? `<button class="agent-card-btn doc-agent-btn" data-task="${agentTask}" data-key="${f.key}" style="margin-top:8px">${agentLabel}</button>`
|
|
3655
|
+
: `<div style="margin-top:8px;font-size:11px;color:var(--green)">Документация актуальна</div>`;
|
|
3656
|
+
|
|
3657
|
+
html += `
|
|
3658
|
+
<div class="feature-card doc-feature-card" data-key="${f.key}">
|
|
3659
|
+
<div class="feature-accent" style="background:${f.color}"></div>
|
|
3660
|
+
<div class="feature-body">
|
|
3661
|
+
<div class="feature-title">
|
|
3662
|
+
<span>${escapeHtml(f.label)}</span>
|
|
3663
|
+
<span class="feature-file-count">${f.sourceFileCount} файл.</span>
|
|
3664
|
+
</div>
|
|
3665
|
+
<div style="display:flex;align-items:center;gap:4px;margin:6px 0;font-size:12px">
|
|
3666
|
+
${statusDot}<span style="color:${statusColor};font-weight:600">${statusText}</span>
|
|
3667
|
+
</div>
|
|
3668
|
+
<div style="font-size:11px;color:var(--muted);margin-bottom:6px">Обновлено: ${lastUpd}</div>
|
|
3669
|
+
<div class="feature-progress-bar">
|
|
3670
|
+
<div class="feature-progress-fill" style="width:${f.docExists ? progressPct : 0}%;background:${progressColor}"></div>
|
|
3671
|
+
</div>
|
|
3672
|
+
<div class="feature-progress-label">${f.docExists ? (f.isStale ? `${f.changedFilesSinceDoc.length} из ${f.sourceFileCount} изменены` : `${f.sourceFileCount} из ${f.sourceFileCount} актуальны`) : 'Документация отсутствует'}</div>
|
|
3673
|
+
${agentBtn}
|
|
3674
|
+
</div>
|
|
3675
|
+
</div>`;
|
|
3676
|
+
}
|
|
3677
|
+
html += '</div>';
|
|
3678
|
+
c.innerHTML = html;
|
|
3679
|
+
|
|
3680
|
+
// Card click → drill-down
|
|
3681
|
+
c.querySelectorAll('.doc-feature-card').forEach(card => {
|
|
3682
|
+
card.addEventListener('click', (e) => {
|
|
3683
|
+
if (e.target.closest('.doc-agent-btn')) return;
|
|
3684
|
+
setFeatureDrill(card.dataset.key);
|
|
3685
|
+
});
|
|
3686
|
+
});
|
|
3687
|
+
|
|
3688
|
+
// Agent button clicks
|
|
3689
|
+
c.querySelectorAll('.doc-agent-btn').forEach(btn => {
|
|
3690
|
+
btn.addEventListener('click', (e) => {
|
|
3691
|
+
e.stopPropagation();
|
|
3692
|
+
runAgentTask(btn.dataset.task, btn.dataset.key);
|
|
3693
|
+
});
|
|
3694
|
+
});
|
|
3695
|
+
}
|
|
3696
|
+
|
|
3697
|
+
// Lightweight markdown → HTML renderer
|
|
3698
|
+
function renderMarkdownToHtml(md) {
|
|
3699
|
+
let html = '';
|
|
3700
|
+
const lines = md.split('\n');
|
|
3701
|
+
let inCodeBlock = false;
|
|
3702
|
+
let codeBlockContent = '';
|
|
3703
|
+
let inList = false;
|
|
3704
|
+
let listType = '';
|
|
3705
|
+
|
|
3706
|
+
for (let i = 0; i < lines.length; i++) {
|
|
3707
|
+
const line = lines[i];
|
|
3708
|
+
|
|
3709
|
+
// Code blocks
|
|
3710
|
+
if (line.trimStart().startsWith('```')) {
|
|
3711
|
+
if (inCodeBlock) {
|
|
3712
|
+
html += `<pre class="doc-code"><code>${escapeHtml(codeBlockContent.trimEnd())}</code></pre>`;
|
|
3713
|
+
codeBlockContent = '';
|
|
3714
|
+
inCodeBlock = false;
|
|
3715
|
+
} else {
|
|
3716
|
+
if (inList) { html += listType === 'ul' ? '</ul>' : '</ol>'; inList = false; }
|
|
3717
|
+
inCodeBlock = true;
|
|
3718
|
+
}
|
|
3719
|
+
continue;
|
|
3720
|
+
}
|
|
3721
|
+
if (inCodeBlock) {
|
|
3722
|
+
codeBlockContent += line + '\n';
|
|
3723
|
+
continue;
|
|
3724
|
+
}
|
|
3725
|
+
|
|
3726
|
+
// Headings
|
|
3727
|
+
const headingMatch = line.match(/^(#{1,6})\s+(.+)$/);
|
|
3728
|
+
if (headingMatch) {
|
|
3729
|
+
if (inList) { html += listType === 'ul' ? '</ul>' : '</ol>'; inList = false; }
|
|
3730
|
+
const level = headingMatch[1].length;
|
|
3731
|
+
html += `<h${level} class="doc-h">${inlineFormat(headingMatch[2])}</h${level}>`;
|
|
3732
|
+
continue;
|
|
3733
|
+
}
|
|
3734
|
+
|
|
3735
|
+
// Unordered list
|
|
3736
|
+
if (/^\s*[-*]\s+/.test(line)) {
|
|
3737
|
+
if (!inList || listType !== 'ul') {
|
|
3738
|
+
if (inList) html += listType === 'ul' ? '</ul>' : '</ol>';
|
|
3739
|
+
html += '<ul class="doc-list">';
|
|
3740
|
+
inList = true;
|
|
3741
|
+
listType = 'ul';
|
|
3742
|
+
}
|
|
3743
|
+
html += `<li>${inlineFormat(line.replace(/^\s*[-*]\s+/, ''))}</li>`;
|
|
3744
|
+
continue;
|
|
3745
|
+
}
|
|
3746
|
+
|
|
3747
|
+
// Ordered list
|
|
3748
|
+
if (/^\s*\d+\.\s+/.test(line)) {
|
|
3749
|
+
if (!inList || listType !== 'ol') {
|
|
3750
|
+
if (inList) html += listType === 'ul' ? '</ul>' : '</ol>';
|
|
3751
|
+
html += '<ol class="doc-list">';
|
|
3752
|
+
inList = true;
|
|
3753
|
+
listType = 'ol';
|
|
3754
|
+
}
|
|
3755
|
+
html += `<li>${inlineFormat(line.replace(/^\s*\d+\.\s+/, ''))}</li>`;
|
|
3756
|
+
continue;
|
|
3757
|
+
}
|
|
3758
|
+
|
|
3759
|
+
// End list
|
|
3760
|
+
if (inList && line.trim() === '') {
|
|
3761
|
+
html += listType === 'ul' ? '</ul>' : '</ol>';
|
|
3762
|
+
inList = false;
|
|
3763
|
+
}
|
|
3764
|
+
|
|
3765
|
+
// Empty line
|
|
3766
|
+
if (line.trim() === '') continue;
|
|
3767
|
+
|
|
3768
|
+
// Paragraph
|
|
3769
|
+
if (!inList) {
|
|
3770
|
+
html += `<p class="doc-p">${inlineFormat(line)}</p>`;
|
|
3771
|
+
}
|
|
3772
|
+
}
|
|
3773
|
+
|
|
3774
|
+
if (inCodeBlock) {
|
|
3775
|
+
html += `<pre class="doc-code"><code>${escapeHtml(codeBlockContent.trimEnd())}</code></pre>`;
|
|
3776
|
+
}
|
|
3777
|
+
if (inList) html += listType === 'ul' ? '</ul>' : '</ol>';
|
|
3778
|
+
return html;
|
|
3779
|
+
}
|
|
3780
|
+
|
|
3781
|
+
function inlineFormat(text) {
|
|
3782
|
+
let s = escapeHtml(text);
|
|
3783
|
+
s = s.replace(/`([^`]+)`/g, '<code class="doc-inline-code">$1</code>');
|
|
3784
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>');
|
|
3785
|
+
s = s.replace(/\*([^*]+)\*/g, '<em>$1</em>');
|
|
3786
|
+
s = s.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2" target="_blank" style="color:var(--blue)">$1</a>');
|
|
3787
|
+
return s;
|
|
3788
|
+
}
|
|
3789
|
+
|
|
3790
|
+
let docContentCache = {};
|
|
3791
|
+
|
|
3792
|
+
async function renderDocFeatureDetail(c) {
|
|
3793
|
+
const doc = D.documentation;
|
|
3794
|
+
const f = doc?.features.find(x => x.key === drillFeatureKey);
|
|
3795
|
+
if (!f) {
|
|
3796
|
+
c.innerHTML = `<div class="onboarding-block"><p>Фича не найдена.</p></div>`;
|
|
3797
|
+
return;
|
|
3798
|
+
}
|
|
3799
|
+
|
|
3800
|
+
const statusColor = !f.docExists ? 'var(--red)' : f.isStale ? 'var(--yellow)' : 'var(--green)';
|
|
3801
|
+
const statusText = !f.docExists ? 'Отсутствует' : f.isStale ? `Устарела (${f.changedFilesSinceDoc.length} файл.)` : 'Актуальна';
|
|
3802
|
+
const statusDot = `<span style="display:inline-block;width:8px;height:8px;border-radius:50%;background:${statusColor};margin-right:6px"></span>`;
|
|
3803
|
+
const lastUpd = f.lastUpdated ? new Date(f.lastUpdated).toLocaleDateString('ru-RU') : '—';
|
|
3804
|
+
|
|
3805
|
+
const agentTask = !f.docExists ? 'generate-docs' : f.isStale ? 'update-docs' : null;
|
|
3806
|
+
const agentLabel = !f.docExists ? 'Сгенерировать документацию' : f.isStale ? `Обновить документацию (${f.changedFilesSinceDoc.length} файл.)` : '';
|
|
3807
|
+
|
|
3808
|
+
let changedFilesHtml = '';
|
|
3809
|
+
if (f.isStale && f.changedFilesSinceDoc.length > 0) {
|
|
3810
|
+
changedFilesHtml = `
|
|
3811
|
+
<div class="doc-changed-section">
|
|
3812
|
+
<div class="doc-changed-header" onclick="this.parentElement.classList.toggle('open')">
|
|
3813
|
+
Изменённые файлы (${f.changedFilesSinceDoc.length})
|
|
3814
|
+
<span class="doc-changed-arrow">▶</span>
|
|
3815
|
+
</div>
|
|
3816
|
+
<div class="doc-changed-list">
|
|
3817
|
+
${f.changedFilesSinceDoc.map(fp => `<div class="doc-changed-file">${escapeHtml(fp)}</div>`).join('')}
|
|
3818
|
+
</div>
|
|
3819
|
+
</div>`;
|
|
3820
|
+
}
|
|
3821
|
+
|
|
3822
|
+
let docContentHtml = '';
|
|
3823
|
+
if (f.docExists) {
|
|
3824
|
+
// Fetch content if not cached
|
|
3825
|
+
if (!docContentCache[f.key]) {
|
|
3826
|
+
try {
|
|
3827
|
+
const resp = await fetch('/api/docs/content?feature=' + encodeURIComponent(f.key));
|
|
3828
|
+
const data = await resp.json();
|
|
3829
|
+
if (data.exists && data.content) docContentCache[f.key] = data.content;
|
|
3830
|
+
} catch {}
|
|
3831
|
+
}
|
|
3832
|
+
const raw = docContentCache[f.key] || '';
|
|
3833
|
+
docContentHtml = raw ? `<div class="doc-content">${renderMarkdownToHtml(raw)}</div>` : '<div style="color:var(--muted);padding:16px">Не удалось загрузить содержимое документации.</div>';
|
|
3834
|
+
} else {
|
|
3835
|
+
docContentHtml = `<div style="color:var(--muted);padding:16px;text-align:center">Документация ещё не создана. Нажмите кнопку ниже, чтобы сгенерировать.</div>`;
|
|
3836
|
+
}
|
|
3837
|
+
|
|
3838
|
+
c.innerHTML = `
|
|
3839
|
+
<div style="margin-bottom:16px">
|
|
3840
|
+
<button class="back-btn" id="docBackBtn">← Все фичи</button>
|
|
3841
|
+
</div>
|
|
3842
|
+
<div style="display:flex;align-items:center;gap:12px;margin-bottom:12px">
|
|
3843
|
+
<div style="width:4px;height:28px;border-radius:2px;background:${f.color}"></div>
|
|
3844
|
+
<h2 style="margin:0;font-size:18px;color:var(--text)">${escapeHtml(f.label)}</h2>
|
|
3845
|
+
<div style="display:flex;align-items:center;font-size:13px">${statusDot}<span style="color:${statusColor};font-weight:600">${statusText}</span></div>
|
|
3846
|
+
<span style="font-size:12px;color:var(--muted)">Обновлено: ${lastUpd}</span>
|
|
3847
|
+
</div>
|
|
3848
|
+
${agentTask ? `<div style="margin-bottom:16px"><button class="agent-card-btn doc-detail-agent-btn" data-task="${agentTask}" data-key="${f.key}">${agentLabel}</button></div>` : ''}
|
|
3849
|
+
${changedFilesHtml}
|
|
3850
|
+
${docContentHtml}`;
|
|
3851
|
+
|
|
3852
|
+
document.getElementById('docBackBtn').onclick = () => backToFeatures();
|
|
3853
|
+
|
|
3854
|
+
const detailAgentBtn = c.querySelector('.doc-detail-agent-btn');
|
|
3855
|
+
if (detailAgentBtn) {
|
|
3856
|
+
detailAgentBtn.onclick = () => runAgentTask(detailAgentBtn.dataset.task, detailAgentBtn.dataset.key);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
|
|
3534
3860
|
function renderFeatureCards(c) {
|
|
3535
3861
|
if (!D.hasConfig || !D.features) {
|
|
3536
3862
|
c.innerHTML = `
|
|
@@ -4637,6 +4963,7 @@ async function refreshData() {
|
|
|
4637
4963
|
try {
|
|
4638
4964
|
const res = await fetch('/api/data');
|
|
4639
4965
|
D = await res.json();
|
|
4966
|
+
docContentCache = {};
|
|
4640
4967
|
|
|
4641
4968
|
// Update header timestamp
|
|
4642
4969
|
document.getElementById('scannedAt').textContent =
|