@eltonssouza/development-utility-kit 1.0.0
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/.claude/agents/analyst.md +198 -0
- package/.claude/agents/backend-developer.md +126 -0
- package/.claude/agents/brain-keeper.md +229 -0
- package/.claude/agents/code-reviewer.md +181 -0
- package/.claude/agents/database-engineer.md +94 -0
- package/.claude/agents/devops-engineer.md +141 -0
- package/.claude/agents/frontend-developer.md +97 -0
- package/.claude/agents/gate-keeper.md +118 -0
- package/.claude/agents/migrator.md +291 -0
- package/.claude/agents/mobile-developer.md +80 -0
- package/.claude/agents/n8n-specialist.md +94 -0
- package/.claude/agents/product-owner.md +115 -0
- package/.claude/agents/qa-engineer.md +232 -0
- package/.claude/agents/release-engineer.md +204 -0
- package/.claude/agents/scaffold.md +87 -0
- package/.claude/agents/security-engineer.md +199 -0
- package/.claude/agents/sprint-runner.md +44 -0
- package/.claude/agents/stack-resolver.md +84 -0
- package/.claude/agents/tech-lead.md +182 -0
- package/.claude/agents/update-template.md +54 -0
- package/.claude/agents/ux-designer.md +118 -0
- package/.claude/settings.json +44 -0
- package/.claude/skills/README.md +332 -0
- package/.claude/skills/active-project/SKILL.md +129 -0
- package/.claude/skills/api-integration-test/SKILL.md +64 -0
- package/.claude/skills/auto-test-guard/SKILL.md +237 -0
- package/.claude/skills/auto-test-guard/resources/backend-tests.md +20 -0
- package/.claude/skills/auto-test-guard/resources/e2e-tests.md +24 -0
- package/.claude/skills/auto-test-guard/resources/execution-report.md +49 -0
- package/.claude/skills/auto-test-guard/resources/frontend-tests.md +18 -0
- package/.claude/skills/auto-test-guard/resources/initial-setup.md +108 -0
- package/.claude/skills/auto-test-guard/resources/run-suite.md +48 -0
- package/.claude/skills/auto-test-guard/resources/senior-gate.md +19 -0
- package/.claude/skills/brain-keeper/SKILL.md +60 -0
- package/.claude/skills/brain-keeper/obsidian/app.json +9 -0
- package/.claude/skills/brain-keeper/obsidian/appearance.json +4 -0
- package/.claude/skills/brain-keeper/obsidian/core-plugins.json +20 -0
- package/.claude/skills/brain-keeper/obsidian/daily-notes.json +5 -0
- package/.claude/skills/brain-keeper/obsidian/graph.json +32 -0
- package/.claude/skills/brain-keeper/obsidian/snippets/folder-colors.css +90 -0
- package/.claude/skills/brain-keeper/obsidian/templates.json +5 -0
- package/.claude/skills/brain-keeper/templates/README.md +51 -0
- package/.claude/skills/brain-keeper/templates/adr.md +40 -0
- package/.claude/skills/brain-keeper/templates/bug.md +35 -0
- package/.claude/skills/brain-keeper/templates/daily.md +38 -0
- package/.claude/skills/brain-keeper/templates/feature.md +62 -0
- package/.claude/skills/brain-keeper/templates/meeting.md +34 -0
- package/.claude/skills/brain-keeper/templates/tech-debt.md +21 -0
- package/.claude/skills/caveman/SKILL.md +187 -0
- package/.claude/skills/create-stack-pack/SKILL.md +281 -0
- package/.claude/skills/grill-me/SKILL.md +79 -0
- package/.claude/skills/honcho-memory/SKILL.md +207 -0
- package/.claude/skills/honcho-memory/docs/api-endpoints-verified.md +75 -0
- package/.claude/skills/honcho-memory/hooks/on-prompt-submit.js +221 -0
- package/.claude/skills/honcho-memory/hooks/on-stop.js +193 -0
- package/.claude/skills/honcho-memory/lib/honcho-client.js +363 -0
- package/.claude/skills/honcho-memory/lib/memory-injector.js +93 -0
- package/.claude/skills/honcho-memory/package.json +32 -0
- package/.claude/skills/honcho-memory/scripts/cli.js +370 -0
- package/.claude/skills/honcho-memory/scripts/setup.js +109 -0
- package/.claude/skills/honcho-memory/tests/t001-api-endpoints-verified.test.js +89 -0
- package/.claude/skills/honcho-memory/tests/t002-structure.test.js +97 -0
- package/.claude/skills/honcho-memory/tests/t003-honcho-client.test.js +162 -0
- package/.claude/skills/honcho-memory/tests/t004-soft-delete.test.js +259 -0
- package/.claude/skills/honcho-memory/tests/t005-memory-injector.test.js +175 -0
- package/.claude/skills/honcho-memory/tests/t006-on-prompt-submit.test.js +215 -0
- package/.claude/skills/honcho-memory/tests/t007-on-stop.test.js +165 -0
- package/.claude/skills/honcho-memory/tests/t008-cli.test.js +214 -0
- package/.claude/skills/honcho-memory/tests/t009-setup.test.js +232 -0
- package/.claude/skills/honcho-memory/tests/t010-skill-md.test.js +114 -0
- package/.claude/skills/honcho-memory/tests/t011-settings-hooks.test.js +105 -0
- package/.claude/skills/honcho-memory/tests/t012-docs-update.test.js +106 -0
- package/.claude/skills/honcho-memory/tests/t013-smoke-e2e.test.js +90 -0
- package/.claude/skills/pair-debug/SKILL.md +288 -0
- package/.claude/skills/prd-ready-check/SKILL.md +58 -0
- package/.claude/skills/project-manager/SKILL.md +167 -0
- package/.claude/skills/quality-standards/SKILL.md +201 -0
- package/.claude/skills/quick-feature/SKILL.md +264 -0
- package/.claude/skills/run-sprint/SKILL.md +342 -0
- package/.claude/skills/scaffold/SKILL.md +58 -0
- package/.claude/skills/stack-discovery/SKILL.md +159 -0
- package/.claude/skills/test-coverage-auditor/SKILL.md +59 -0
- package/.claude/skills/to-issues/SKILL.md +163 -0
- package/.claude/skills/to-prd/SKILL.md +130 -0
- package/.claude/skills/update-template/SKILL.md +254 -0
- package/.claude/stacks/CODEOWNERS +30 -0
- package/.claude/stacks/README.md +88 -0
- package/.claude/stacks/_template.md +116 -0
- package/.claude/stacks/java/spring-boot-3.md +376 -0
- package/.claude/stacks/java/spring-boot-4.md +438 -0
- package/.claude/stacks/typescript/angular-18.md +420 -0
- package/.claude/stacks/typescript/angular-19.md +397 -0
- package/.claude/stacks/typescript/angular-21.md +494 -0
- package/CLAUDE.md +453 -0
- package/README.md +391 -0
- package/bin/cli.js +773 -0
- package/bin/lib/backup.js +62 -0
- package/bin/lib/detect-stack.js +476 -0
- package/bin/lib/help.js +233 -0
- package/bin/lib/identity.js +108 -0
- package/bin/lib/local-dir.js +69 -0
- package/bin/lib/manifest.js +236 -0
- package/bin/lib/sync-all.js +394 -0
- package/bin/lib/version-check.js +398 -0
- package/dashboard/db.js +199 -0
- package/dashboard/package.json +22 -0
- package/dashboard/public/app.js +709 -0
- package/dashboard/public/content/docs/agents-reference.en.md +911 -0
- package/dashboard/public/content/docs/architecture-overview.en.md +260 -0
- package/dashboard/public/content/docs/autonomy-matrix.en.md +186 -0
- package/dashboard/public/content/docs/git-flow.en.md +525 -0
- package/dashboard/public/content/docs/honcho-memory.en.md +394 -0
- package/dashboard/public/content/docs/hooks-reference.en.md +420 -0
- package/dashboard/public/content/docs/pipeline.en.md +400 -0
- package/dashboard/public/content/docs/quality-gate.en.md +315 -0
- package/dashboard/public/content/docs/skills-reference.en.md +500 -0
- package/dashboard/public/content/docs/stack-rules.en.md +362 -0
- package/dashboard/public/content/docs/troubleshooting.en.md +637 -0
- package/dashboard/public/content/manifest.json +102 -0
- package/dashboard/public/content/manual/backend.en.md +1138 -0
- package/dashboard/public/content/manual/existing-project.en.md +831 -0
- package/dashboard/public/content/manual/frontend.en.md +1065 -0
- package/dashboard/public/content/manual/fullstack.en.md +1508 -0
- package/dashboard/public/content/manual/mobile.en.md +866 -0
- package/dashboard/public/index.html +108 -0
- package/dashboard/public/style.css +610 -0
- package/dashboard/public/vendor/marked.min.js +69 -0
- package/dashboard/rtk.js +143 -0
- package/dashboard/server-app.js +403 -0
- package/dashboard/server.js +104 -0
- package/dashboard/test/sprint1.test.js +406 -0
- package/dashboard/test/sprint2.test.js +571 -0
- package/dashboard/test/sprint3.test.js +560 -0
- package/package.json +33 -0
- package/scripts/hooks/subagent-telemetry.sh +14 -0
- package/scripts/hooks/telemetry-writer.js +250 -0
- package/scripts/latest-versions.json +56 -0
|
@@ -0,0 +1,709 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ── Chart instances (kept in module scope for update-in-place) ────────────────
|
|
4
|
+
|
|
5
|
+
let modelsChart = null;
|
|
6
|
+
let rtkChart = null;
|
|
7
|
+
let pollIntervalId = null;
|
|
8
|
+
|
|
9
|
+
// ── Tab engine ────────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
let activeTab = 'dashboard';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Activate the given tab panel by ID, update URL hash and button states.
|
|
15
|
+
* Pauses/resumes the stats polling based on whether dashboard tab is active (IR-2).
|
|
16
|
+
* @param {string} tabId — 'dashboard' | 'manual' | 'docs'
|
|
17
|
+
*/
|
|
18
|
+
function activateTab(tabId) {
|
|
19
|
+
const validTabs = ['dashboard', 'manual', 'docs'];
|
|
20
|
+
if (!validTabs.includes(tabId)) tabId = 'dashboard';
|
|
21
|
+
|
|
22
|
+
activeTab = tabId;
|
|
23
|
+
|
|
24
|
+
// Update tab buttons
|
|
25
|
+
document.querySelectorAll('.tab-btn').forEach((btn) => {
|
|
26
|
+
const isActive = btn.dataset.tab === tabId;
|
|
27
|
+
btn.classList.toggle('active', isActive);
|
|
28
|
+
btn.setAttribute('aria-selected', String(isActive));
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Toggle panels
|
|
32
|
+
document.querySelectorAll('.tab-panel').forEach((panel) => {
|
|
33
|
+
const panelTab = panel.id.replace('panel-', '');
|
|
34
|
+
if (panelTab === tabId) {
|
|
35
|
+
panel.removeAttribute('hidden');
|
|
36
|
+
panel.classList.add('active');
|
|
37
|
+
} else {
|
|
38
|
+
panel.setAttribute('hidden', '');
|
|
39
|
+
panel.classList.remove('active');
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Pause polling when NOT on dashboard (IR-2)
|
|
44
|
+
if (tabId === 'dashboard') {
|
|
45
|
+
resumePolling();
|
|
46
|
+
} else {
|
|
47
|
+
pausePolling();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Lazy-render panel content on first activation
|
|
51
|
+
if (tabId === 'manual') {
|
|
52
|
+
initManualPanel();
|
|
53
|
+
} else if (tabId === 'docs') {
|
|
54
|
+
initDocsPanel();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ── Polling control ───────────────────────────────────────────────────────────
|
|
59
|
+
|
|
60
|
+
function pausePolling() {
|
|
61
|
+
if (pollIntervalId !== null) {
|
|
62
|
+
clearInterval(pollIntervalId);
|
|
63
|
+
pollIntervalId = null;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function resumePolling() {
|
|
68
|
+
if (pollIntervalId === null) {
|
|
69
|
+
fetchStats();
|
|
70
|
+
pollIntervalId = setInterval(fetchStats, 5000);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ── TOC builder ───────────────────────────────────────────────────────────────
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Build a TOC list from all h2 elements inside containerEl.
|
|
78
|
+
* Injects the result into tocEl.
|
|
79
|
+
* @param {Element} containerEl — content body element
|
|
80
|
+
* @param {Element} tocEl — sidebar element to populate
|
|
81
|
+
*/
|
|
82
|
+
function buildToc(containerEl, tocEl) {
|
|
83
|
+
if (!tocEl) return;
|
|
84
|
+
const headings = Array.from(containerEl.querySelectorAll('h2'));
|
|
85
|
+
if (headings.length === 0) {
|
|
86
|
+
tocEl.innerHTML = '';
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const ul = document.createElement('ul');
|
|
91
|
+
headings.forEach((h, idx) => {
|
|
92
|
+
// Ensure each heading has an id for anchor linking
|
|
93
|
+
if (!h.id) {
|
|
94
|
+
h.id = 'toc-heading-' + idx + '-' + h.textContent.trim().toLowerCase().replace(/\s+/g, '-').replace(/[^\w-]/g, '');
|
|
95
|
+
}
|
|
96
|
+
const li = document.createElement('li');
|
|
97
|
+
const a = document.createElement('a');
|
|
98
|
+
a.href = '#' + h.id;
|
|
99
|
+
a.textContent = h.textContent;
|
|
100
|
+
li.appendChild(a);
|
|
101
|
+
ul.appendChild(li);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
tocEl.innerHTML = '';
|
|
105
|
+
tocEl.appendChild(ul);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// ── Markdown loader with language fallback ────────────────────────────────────
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Fetch a markdown file via /api/docs/file endpoint.
|
|
112
|
+
* Returns { content: string, fallback: boolean }.
|
|
113
|
+
* @param {string} relPath — repo-relative path
|
|
114
|
+
* @returns {Promise<{content: string, fallback: boolean}>}
|
|
115
|
+
*/
|
|
116
|
+
async function fetchMd(relPath) {
|
|
117
|
+
const res = await fetch('/api/docs/file?path=' + encodeURIComponent(relPath));
|
|
118
|
+
if (!res.ok) throw new Error('HTTP ' + res.status + ' for ' + relPath);
|
|
119
|
+
const content = await res.text();
|
|
120
|
+
return { content, fallback: false };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Load a markdown content file. Single-language (English-only) as of v2.0.0.
|
|
125
|
+
* @param {string} path — relative path to the .md file
|
|
126
|
+
* @returns {Promise<{content: string, fallback: boolean}>}
|
|
127
|
+
*/
|
|
128
|
+
async function loadMarkdown(path) {
|
|
129
|
+
return await fetchMd(path);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Render markdown string to safe HTML using marked (loaded from CDN or vendor fallback).
|
|
134
|
+
* @param {string} mdText
|
|
135
|
+
* @returns {string} HTML string
|
|
136
|
+
*/
|
|
137
|
+
function renderMarkdown(mdText) {
|
|
138
|
+
if (typeof marked !== 'undefined' && marked.parse) {
|
|
139
|
+
return marked.parse(mdText);
|
|
140
|
+
}
|
|
141
|
+
// Minimal fallback if marked is not available yet
|
|
142
|
+
return '<pre>' + mdText.replace(/</g, '<').replace(/>/g, '>') + '</pre>';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── Accordion builder ─────────────────────────────────────────────────────────
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Wrap sections (h2 + following content) in accordion elements.
|
|
149
|
+
* "Quick-start" heading stays visible (not wrapped). All other h2 sections become accordions.
|
|
150
|
+
* @param {Element} containerEl
|
|
151
|
+
*/
|
|
152
|
+
function buildAccordions(containerEl) {
|
|
153
|
+
const children = Array.from(containerEl.childNodes);
|
|
154
|
+
const result = document.createDocumentFragment();
|
|
155
|
+
let currentAccordion = null;
|
|
156
|
+
let currentBody = null;
|
|
157
|
+
let isInsideAccordion = false;
|
|
158
|
+
let isQuickStart = false;
|
|
159
|
+
|
|
160
|
+
for (const node of children) {
|
|
161
|
+
if (node.nodeType === Node.ELEMENT_NODE && node.tagName === 'H2') {
|
|
162
|
+
const text = node.textContent.trim().toLowerCase();
|
|
163
|
+
isQuickStart = text.startsWith('quick-start') || text.startsWith('quickstart');
|
|
164
|
+
|
|
165
|
+
if (isQuickStart) {
|
|
166
|
+
// Quick-start: render directly, not in accordion
|
|
167
|
+
currentAccordion = null;
|
|
168
|
+
currentBody = null;
|
|
169
|
+
isInsideAccordion = false;
|
|
170
|
+
result.appendChild(node.cloneNode(true));
|
|
171
|
+
} else {
|
|
172
|
+
// Wrap in accordion
|
|
173
|
+
currentAccordion = document.createElement('div');
|
|
174
|
+
currentAccordion.className = 'accordion';
|
|
175
|
+
|
|
176
|
+
const header = document.createElement('button');
|
|
177
|
+
header.className = 'accordion-header';
|
|
178
|
+
header.innerHTML = node.innerHTML + '<span class="accordion-chevron">▼</span>';
|
|
179
|
+
|
|
180
|
+
const body = document.createElement('div');
|
|
181
|
+
body.className = 'accordion-body';
|
|
182
|
+
|
|
183
|
+
// Capture `body` locally so each click handler toggles its own panel
|
|
184
|
+
// (without this, all handlers share the outer `currentBody` which ends up
|
|
185
|
+
// pointing at the LAST accordion — clicking any header opens only the last)
|
|
186
|
+
header.addEventListener('click', () => {
|
|
187
|
+
header.classList.toggle('open');
|
|
188
|
+
body.classList.toggle('open');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
currentBody = body;
|
|
192
|
+
|
|
193
|
+
currentAccordion.appendChild(header);
|
|
194
|
+
currentAccordion.appendChild(currentBody);
|
|
195
|
+
result.appendChild(currentAccordion);
|
|
196
|
+
isInsideAccordion = true;
|
|
197
|
+
}
|
|
198
|
+
} else if (isInsideAccordion && currentBody) {
|
|
199
|
+
currentBody.appendChild(node.cloneNode(true));
|
|
200
|
+
} else {
|
|
201
|
+
result.appendChild(node.cloneNode(true));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
containerEl.innerHTML = '';
|
|
206
|
+
containerEl.appendChild(result);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// ── Session cache for API list responses ──────────────────────────────────────
|
|
210
|
+
|
|
211
|
+
function getCached(key) {
|
|
212
|
+
try {
|
|
213
|
+
const raw = sessionStorage.getItem(key);
|
|
214
|
+
return raw ? JSON.parse(raw) : null;
|
|
215
|
+
} catch {
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function setCache(key, data) {
|
|
221
|
+
try {
|
|
222
|
+
sessionStorage.setItem(key, JSON.stringify(data));
|
|
223
|
+
} catch {
|
|
224
|
+
// sessionStorage not available — silently ignore
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// ── Manual panel ──────────────────────────────────────────────────────────────
|
|
229
|
+
|
|
230
|
+
let manualInitialized = false;
|
|
231
|
+
let currentScenarioPath = null;
|
|
232
|
+
|
|
233
|
+
async function initManualPanel() {
|
|
234
|
+
if (manualInitialized) return;
|
|
235
|
+
manualInitialized = true;
|
|
236
|
+
|
|
237
|
+
const selectorEl = document.getElementById('manual-scenario-selector');
|
|
238
|
+
const contentBody = document.getElementById('manual-content-body');
|
|
239
|
+
const bannerEl = document.getElementById('translation-banner-manual');
|
|
240
|
+
const tocEl = document.getElementById('toc-manual');
|
|
241
|
+
|
|
242
|
+
// Load manifest (served as static file via /api/docs/file)
|
|
243
|
+
let manifest;
|
|
244
|
+
try {
|
|
245
|
+
const res = await fetch('/dashboard/public/content/manifest.json');
|
|
246
|
+
if (!res.ok) {
|
|
247
|
+
// Try direct static path
|
|
248
|
+
const res2 = await fetch('/content/manifest.json');
|
|
249
|
+
if (!res2.ok) throw new Error('Cannot load manifest');
|
|
250
|
+
manifest = await res2.json();
|
|
251
|
+
} else {
|
|
252
|
+
manifest = await res.json();
|
|
253
|
+
}
|
|
254
|
+
} catch {
|
|
255
|
+
if (contentBody) contentBody.innerHTML = '<p class="empty-state">Error loading manifest.</p>';
|
|
256
|
+
return;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (!selectorEl) return;
|
|
260
|
+
|
|
261
|
+
selectorEl.innerHTML = '';
|
|
262
|
+
for (const entry of manifest.manual) {
|
|
263
|
+
const btn = document.createElement('button');
|
|
264
|
+
btn.className = 'scenario-btn';
|
|
265
|
+
btn.dataset.slug = entry.slug || entry.id;
|
|
266
|
+
btn.dataset.path = entry.path;
|
|
267
|
+
btn.textContent = entry.title;
|
|
268
|
+
btn.addEventListener('click', () => loadScenario(btn, manifest));
|
|
269
|
+
selectorEl.appendChild(btn);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function loadScenario(btn, manifest) {
|
|
274
|
+
const contentBody = document.getElementById('manual-content-body');
|
|
275
|
+
const bannerEl = document.getElementById('translation-banner-manual');
|
|
276
|
+
const tocEl = document.getElementById('toc-manual');
|
|
277
|
+
|
|
278
|
+
// Update active button state
|
|
279
|
+
document.querySelectorAll('#manual-scenario-selector .scenario-btn').forEach((b) => b.classList.remove('active'));
|
|
280
|
+
btn.classList.add('active');
|
|
281
|
+
|
|
282
|
+
currentScenarioPath = btn.dataset.path;
|
|
283
|
+
|
|
284
|
+
if (contentBody) contentBody.innerHTML = '<div class="loading-spinner">Loading…</div>';
|
|
285
|
+
if (bannerEl) bannerEl.hidden = true;
|
|
286
|
+
|
|
287
|
+
try {
|
|
288
|
+
const { content, fallback } = await loadMarkdown(currentScenarioPath);
|
|
289
|
+
if (bannerEl) bannerEl.hidden = true;
|
|
290
|
+
|
|
291
|
+
const htmlContent = renderMarkdown(content);
|
|
292
|
+
if (contentBody) {
|
|
293
|
+
contentBody.innerHTML = htmlContent;
|
|
294
|
+
buildAccordions(contentBody);
|
|
295
|
+
buildToc(contentBody, tocEl);
|
|
296
|
+
}
|
|
297
|
+
} catch (err) {
|
|
298
|
+
if (contentBody) contentBody.innerHTML = '<p class="empty-state">Error loading content: ' + err.message + '</p>';
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// ── Docs panel ────────────────────────────────────────────────────────────────
|
|
303
|
+
|
|
304
|
+
let docsInitialized = false;
|
|
305
|
+
|
|
306
|
+
async function initDocsPanel() {
|
|
307
|
+
if (docsInitialized) return;
|
|
308
|
+
docsInitialized = true;
|
|
309
|
+
|
|
310
|
+
const contentBody = document.getElementById('docs-content-body');
|
|
311
|
+
const bannerEl = document.getElementById('translation-banner-docs');
|
|
312
|
+
const tocEl = document.getElementById('toc-docs');
|
|
313
|
+
|
|
314
|
+
// Load manifest
|
|
315
|
+
let manifest;
|
|
316
|
+
try {
|
|
317
|
+
const res = await fetch('/content/manifest.json');
|
|
318
|
+
manifest = res.ok ? await res.json() : null;
|
|
319
|
+
if (!manifest) {
|
|
320
|
+
const res2 = await fetch('/dashboard/public/content/manifest.json');
|
|
321
|
+
manifest = res2.ok ? await res2.json() : null;
|
|
322
|
+
}
|
|
323
|
+
if (!manifest) throw new Error('Cannot load manifest');
|
|
324
|
+
} catch {
|
|
325
|
+
if (contentBody) contentBody.innerHTML = '<p class="empty-state">Error loading manifest.</p>';
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (!contentBody) return;
|
|
330
|
+
|
|
331
|
+
// Build a simple nav for docs sections
|
|
332
|
+
const nav = document.createElement('div');
|
|
333
|
+
nav.className = 'scenario-selector';
|
|
334
|
+
for (const entry of manifest.docs) {
|
|
335
|
+
const btn = document.createElement('button');
|
|
336
|
+
btn.className = 'scenario-btn';
|
|
337
|
+
btn.dataset.path = entry.path;
|
|
338
|
+
btn.textContent = entry.title;
|
|
339
|
+
btn.addEventListener('click', () => loadDocsSection(btn, bannerEl, tocEl));
|
|
340
|
+
nav.appendChild(btn);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Dynamic index sections
|
|
344
|
+
const dynamicSections = [
|
|
345
|
+
{ id: 'skills-index', label: 'Skills Index', endpoint: '/api/docs/skills', cacheKey: 'kud_cache_skills', type: 'skills' },
|
|
346
|
+
{ id: 'agents-index', label: 'Agents Index', endpoint: '/api/docs/agents', cacheKey: 'kud_cache_agents', type: 'agents' },
|
|
347
|
+
{ id: 'adrs-timeline', label: 'ADRs Timeline', endpoint: '/api/docs/adrs', cacheKey: 'kud_cache_adrs', type: 'adrs' },
|
|
348
|
+
];
|
|
349
|
+
|
|
350
|
+
for (const section of dynamicSections) {
|
|
351
|
+
const btn = document.createElement('button');
|
|
352
|
+
btn.className = 'scenario-btn';
|
|
353
|
+
btn.dataset.dynamicSection = section.id;
|
|
354
|
+
btn.textContent = section.label;
|
|
355
|
+
btn.addEventListener('click', () => loadDynamicSection(section, contentBody, tocEl));
|
|
356
|
+
nav.appendChild(btn);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
contentBody.innerHTML = '';
|
|
360
|
+
contentBody.appendChild(nav);
|
|
361
|
+
contentBody.insertAdjacentHTML('beforeend', '<div id="docs-section-content"></div>');
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
async function loadDocsSection(btn, bannerEl, tocEl) {
|
|
365
|
+
const sectionContentEl = document.getElementById('docs-section-content');
|
|
366
|
+
if (!sectionContentEl) return;
|
|
367
|
+
|
|
368
|
+
document.querySelectorAll('#docs-content-body .scenario-btn').forEach((b) => b.classList.remove('active'));
|
|
369
|
+
btn.classList.add('active');
|
|
370
|
+
|
|
371
|
+
if (bannerEl) bannerEl.hidden = true;
|
|
372
|
+
sectionContentEl.innerHTML = '<div class="loading-spinner">Loading…</div>';
|
|
373
|
+
|
|
374
|
+
try {
|
|
375
|
+
const { content, fallback } = await loadMarkdown(btn.dataset.path);
|
|
376
|
+
if (bannerEl) bannerEl.hidden = true;
|
|
377
|
+
|
|
378
|
+
const htmlContent = renderMarkdown(content);
|
|
379
|
+
sectionContentEl.innerHTML = htmlContent;
|
|
380
|
+
buildToc(sectionContentEl, tocEl);
|
|
381
|
+
} catch (err) {
|
|
382
|
+
sectionContentEl.innerHTML = '<p class="empty-state">Error: ' + err.message + '</p>';
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function loadDynamicSection(section, contentBody, tocEl) {
|
|
387
|
+
const sectionContentEl = document.getElementById('docs-section-content');
|
|
388
|
+
if (!sectionContentEl) return;
|
|
389
|
+
|
|
390
|
+
document.querySelectorAll('#docs-content-body .scenario-btn').forEach((b) => b.classList.remove('active'));
|
|
391
|
+
// Mark the button active
|
|
392
|
+
contentBody.querySelectorAll('[data-dynamic-section="' + section.id + '"]').forEach((b) => b.classList.add('active'));
|
|
393
|
+
|
|
394
|
+
sectionContentEl.innerHTML = '<div class="loading-spinner">Loading…</div>';
|
|
395
|
+
|
|
396
|
+
// Use sessionStorage cache (IR-4 / NFR-005)
|
|
397
|
+
let data = getCached(section.cacheKey);
|
|
398
|
+
if (!data) {
|
|
399
|
+
try {
|
|
400
|
+
const res = await fetch(section.endpoint);
|
|
401
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
402
|
+
data = await res.json();
|
|
403
|
+
setCache(section.cacheKey, data);
|
|
404
|
+
} catch (err) {
|
|
405
|
+
sectionContentEl.innerHTML = '<p class="empty-state">Error loading ' + section.label + ': ' + err.message + '</p>';
|
|
406
|
+
return;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
sectionContentEl.innerHTML = '';
|
|
411
|
+
const h2 = document.createElement('h2');
|
|
412
|
+
h2.textContent = section.label;
|
|
413
|
+
sectionContentEl.appendChild(h2);
|
|
414
|
+
|
|
415
|
+
const list = document.createElement('ul');
|
|
416
|
+
list.className = 'docs-index-list';
|
|
417
|
+
|
|
418
|
+
for (const entry of data) {
|
|
419
|
+
const li = document.createElement('li');
|
|
420
|
+
li.className = 'docs-index-item';
|
|
421
|
+
|
|
422
|
+
const header = document.createElement('div');
|
|
423
|
+
header.className = 'docs-index-item-header';
|
|
424
|
+
|
|
425
|
+
const name = document.createElement('span');
|
|
426
|
+
name.className = 'docs-index-item-name';
|
|
427
|
+
name.textContent = entry.name || entry.title || (entry.slug ? 'ADR-' + entry.number + ' ' + entry.title : 'Unknown');
|
|
428
|
+
|
|
429
|
+
header.appendChild(name);
|
|
430
|
+
|
|
431
|
+
if (entry.model) {
|
|
432
|
+
const model = document.createElement('span');
|
|
433
|
+
model.className = 'docs-index-item-model';
|
|
434
|
+
model.textContent = entry.model;
|
|
435
|
+
header.appendChild(model);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
if (entry.status) {
|
|
439
|
+
const status = document.createElement('span');
|
|
440
|
+
status.className = 'docs-index-item-model';
|
|
441
|
+
status.textContent = entry.status;
|
|
442
|
+
header.appendChild(status);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const body = document.createElement('div');
|
|
446
|
+
body.className = 'docs-index-item-body';
|
|
447
|
+
body.innerHTML = '<div class="loading-spinner" style="display:none">Loading…</div><div class="content-body"></div>';
|
|
448
|
+
|
|
449
|
+
header.addEventListener('click', async () => {
|
|
450
|
+
const isOpen = body.classList.contains('open');
|
|
451
|
+
body.classList.toggle('open', !isOpen);
|
|
452
|
+
|
|
453
|
+
if (!isOpen && entry.path) {
|
|
454
|
+
const spinner = body.querySelector('.loading-spinner');
|
|
455
|
+
const innerContent = body.querySelector('.content-body');
|
|
456
|
+
if (spinner) spinner.style.display = 'block';
|
|
457
|
+
if (innerContent) innerContent.innerHTML = '';
|
|
458
|
+
|
|
459
|
+
try {
|
|
460
|
+
const res = await fetch('/api/docs/file?path=' + encodeURIComponent(entry.path));
|
|
461
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
462
|
+
const mdText = await res.text();
|
|
463
|
+
if (innerContent) innerContent.innerHTML = renderMarkdown(mdText);
|
|
464
|
+
if (spinner) spinner.style.display = 'none';
|
|
465
|
+
} catch (err) {
|
|
466
|
+
if (innerContent) innerContent.innerHTML = '<p class="empty-state">Error: ' + err.message + '</p>';
|
|
467
|
+
if (spinner) spinner.style.display = 'none';
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
|
|
472
|
+
li.appendChild(header);
|
|
473
|
+
li.appendChild(body);
|
|
474
|
+
list.appendChild(li);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
sectionContentEl.appendChild(list);
|
|
478
|
+
|
|
479
|
+
if (tocEl) tocEl.innerHTML = '';
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// ── Utility: relative date ────────────────────────────────────────────────────
|
|
483
|
+
|
|
484
|
+
function relativeDate(unixSeconds) {
|
|
485
|
+
const now = Math.floor(Date.now() / 1000);
|
|
486
|
+
const diff = now - unixSeconds;
|
|
487
|
+
|
|
488
|
+
if (diff < 86400) return 'today';
|
|
489
|
+
if (diff < 172800) return 'yesterday';
|
|
490
|
+
|
|
491
|
+
const days = Math.floor(diff / 86400);
|
|
492
|
+
if (days < 7) {
|
|
493
|
+
const d = new Date(unixSeconds * 1000);
|
|
494
|
+
return d.toLocaleDateString('en-US', { weekday: 'short' });
|
|
495
|
+
}
|
|
496
|
+
const d = new Date(unixSeconds * 1000);
|
|
497
|
+
return d.toLocaleDateString('en-US', { month: 'short', day: 'numeric' });
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function formatTokens(n) {
|
|
501
|
+
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
502
|
+
if (n >= 1_000) return (n / 1_000).toFixed(0) + 'k';
|
|
503
|
+
return String(n);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// ── Projects card ─────────────────────────────────────────────────────────────
|
|
507
|
+
|
|
508
|
+
function renderProjects(projects) {
|
|
509
|
+
const list = document.getElementById('project-list');
|
|
510
|
+
if (!projects || projects.length === 0) {
|
|
511
|
+
list.innerHTML = '<li class="empty-state">No projects in the last 30 days</li>';
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
list.innerHTML = projects
|
|
516
|
+
.map(
|
|
517
|
+
(p) => `
|
|
518
|
+
<li>
|
|
519
|
+
<span class="project-name" title="${p.path || ''}">${p.name || p.path || 'unknown'}</span>
|
|
520
|
+
<span class="project-meta">
|
|
521
|
+
<span class="project-date">${relativeDate(p.last_seen)}</span>
|
|
522
|
+
<span class="project-sessions">${p.session_count} session${p.session_count !== 1 ? 's' : ''}</span>
|
|
523
|
+
</span>
|
|
524
|
+
</li>`
|
|
525
|
+
)
|
|
526
|
+
.join('');
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// ── Models chart (pie) ────────────────────────────────────────────────────────
|
|
530
|
+
|
|
531
|
+
const MODEL_COLORS = {
|
|
532
|
+
opus: '#f78166',
|
|
533
|
+
sonnet: '#58a6ff',
|
|
534
|
+
haiku: '#39d353',
|
|
535
|
+
other: '#8b949e',
|
|
536
|
+
};
|
|
537
|
+
|
|
538
|
+
function renderModels(models) {
|
|
539
|
+
const canvas = document.getElementById('models-chart');
|
|
540
|
+
if (!canvas) return;
|
|
541
|
+
|
|
542
|
+
const labels = [];
|
|
543
|
+
const data = [];
|
|
544
|
+
const colors = [];
|
|
545
|
+
|
|
546
|
+
for (const [key, val] of Object.entries(models || {})) {
|
|
547
|
+
if (val.count > 0) {
|
|
548
|
+
labels.push(`${key} (${val.pct}%)`);
|
|
549
|
+
data.push(val.count);
|
|
550
|
+
colors.push(MODEL_COLORS[key] || MODEL_COLORS.other);
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (labels.length === 0) {
|
|
555
|
+
labels.push('no data');
|
|
556
|
+
data.push(1);
|
|
557
|
+
colors.push('#30363d');
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (modelsChart) {
|
|
561
|
+
modelsChart.data.labels = labels;
|
|
562
|
+
modelsChart.data.datasets[0].data = data;
|
|
563
|
+
modelsChart.data.datasets[0].backgroundColor = colors;
|
|
564
|
+
modelsChart.update();
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
modelsChart = new Chart(canvas, {
|
|
569
|
+
type: 'pie',
|
|
570
|
+
data: {
|
|
571
|
+
labels,
|
|
572
|
+
datasets: [
|
|
573
|
+
{
|
|
574
|
+
data,
|
|
575
|
+
backgroundColor: colors,
|
|
576
|
+
borderColor: '#161b22',
|
|
577
|
+
borderWidth: 2,
|
|
578
|
+
},
|
|
579
|
+
],
|
|
580
|
+
},
|
|
581
|
+
options: {
|
|
582
|
+
responsive: true,
|
|
583
|
+
maintainAspectRatio: true,
|
|
584
|
+
plugins: {
|
|
585
|
+
legend: {
|
|
586
|
+
position: 'bottom',
|
|
587
|
+
labels: {
|
|
588
|
+
color: '#e6edf3',
|
|
589
|
+
font: { family: 'Consolas, monospace', size: 11 },
|
|
590
|
+
boxWidth: 12,
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
});
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
// ── RTK card ──────────────────────────────────────────────────────────────────
|
|
599
|
+
|
|
600
|
+
function renderRtk(rtk) {
|
|
601
|
+
const tokensEl = document.getElementById('rtk-tokens');
|
|
602
|
+
const pctEl = document.getElementById('rtk-pct');
|
|
603
|
+
const canvas = document.getElementById('rtk-chart');
|
|
604
|
+
|
|
605
|
+
if (!rtk) {
|
|
606
|
+
if (tokensEl) tokensEl.textContent = 'n/a';
|
|
607
|
+
if (pctEl) pctEl.textContent = 'n/a';
|
|
608
|
+
return;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
if (tokensEl) tokensEl.textContent = formatTokens(rtk.tokens_saved || 0);
|
|
612
|
+
if (pctEl) pctEl.textContent = (rtk.savings_pct || 0).toFixed(1) + '%';
|
|
613
|
+
|
|
614
|
+
const daily = Array.isArray(rtk.daily) ? rtk.daily : [];
|
|
615
|
+
const labels = daily.map((d) => d.date);
|
|
616
|
+
const values = daily.map((d) => d.tokens_saved);
|
|
617
|
+
|
|
618
|
+
if (!canvas) return;
|
|
619
|
+
|
|
620
|
+
if (rtkChart) {
|
|
621
|
+
rtkChart.data.labels = labels;
|
|
622
|
+
rtkChart.data.datasets[0].data = values;
|
|
623
|
+
rtkChart.update();
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
rtkChart = new Chart(canvas, {
|
|
628
|
+
type: 'bar',
|
|
629
|
+
data: {
|
|
630
|
+
labels,
|
|
631
|
+
datasets: [
|
|
632
|
+
{
|
|
633
|
+
label: 'tokens saved',
|
|
634
|
+
data: values,
|
|
635
|
+
backgroundColor: '#39d353',
|
|
636
|
+
borderRadius: 3,
|
|
637
|
+
},
|
|
638
|
+
],
|
|
639
|
+
},
|
|
640
|
+
options: {
|
|
641
|
+
responsive: true,
|
|
642
|
+
maintainAspectRatio: true,
|
|
643
|
+
plugins: {
|
|
644
|
+
legend: { display: false },
|
|
645
|
+
},
|
|
646
|
+
scales: {
|
|
647
|
+
x: {
|
|
648
|
+
ticks: { color: '#8b949e', font: { size: 10 } },
|
|
649
|
+
grid: { color: '#21262d' },
|
|
650
|
+
},
|
|
651
|
+
y: {
|
|
652
|
+
ticks: { color: '#8b949e', font: { size: 10 } },
|
|
653
|
+
grid: { color: '#21262d' },
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// ── Main fetch / poll ─────────────────────────────────────────────────────────
|
|
661
|
+
|
|
662
|
+
async function fetchStats() {
|
|
663
|
+
const statusEl = document.getElementById('status-msg');
|
|
664
|
+
const lastUpdatedEl = document.getElementById('last-updated');
|
|
665
|
+
|
|
666
|
+
try {
|
|
667
|
+
const res = await fetch('/api/stats');
|
|
668
|
+
if (!res.ok) throw new Error('HTTP ' + res.status);
|
|
669
|
+
const data = await res.json();
|
|
670
|
+
|
|
671
|
+
renderProjects(data.projects);
|
|
672
|
+
renderModels(data.models);
|
|
673
|
+
renderRtk(data.rtk);
|
|
674
|
+
|
|
675
|
+
const now = new Date().toLocaleTimeString();
|
|
676
|
+
if (statusEl) statusEl.textContent = 'OK';
|
|
677
|
+
if (lastUpdatedEl) lastUpdatedEl.textContent = 'updated ' + now;
|
|
678
|
+
} catch (err) {
|
|
679
|
+
if (statusEl) statusEl.textContent = 'Error: ' + err.message;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
// ── Initialization ────────────────────────────────────────────────────────────
|
|
684
|
+
|
|
685
|
+
document.addEventListener('DOMContentLoaded', () => {
|
|
686
|
+
// Restore active tab from URL hash (IR-6)
|
|
687
|
+
const hash = (window.location.hash || '#dashboard').replace('#', '');
|
|
688
|
+
const validTabs = ['dashboard', 'manual', 'docs'];
|
|
689
|
+
const initialTab = validTabs.includes(hash) ? hash : 'dashboard';
|
|
690
|
+
activateTab(initialTab);
|
|
691
|
+
|
|
692
|
+
// Tab button click handlers
|
|
693
|
+
document.querySelectorAll('.tab-btn').forEach((btn) => {
|
|
694
|
+
btn.addEventListener('click', () => {
|
|
695
|
+
const tabId = btn.dataset.tab;
|
|
696
|
+
window.location.hash = '#' + tabId;
|
|
697
|
+
// hashchange will fire activateTab
|
|
698
|
+
});
|
|
699
|
+
});
|
|
700
|
+
|
|
701
|
+
// Re-activate tab when the URL hash changes (back/forward navigation)
|
|
702
|
+
// Only react to top-level tab hashes; ignore TOC anchors (#toc-heading-*)
|
|
703
|
+
window.addEventListener('hashchange', () => {
|
|
704
|
+
const newHash = (window.location.hash || '#dashboard').replace('#', '');
|
|
705
|
+
if (validTabs.includes(newHash)) {
|
|
706
|
+
activateTab(newHash);
|
|
707
|
+
}
|
|
708
|
+
});
|
|
709
|
+
});
|