@viren/claude-code-dashboard 0.0.1 → 0.0.3
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/README.md +33 -0
- package/generate-dashboard.mjs +96 -13
- package/package.json +1 -1
- package/src/anonymize.mjs +222 -0
- package/src/cli.mjs +14 -5
- package/src/constants.mjs +4 -1
- package/src/demo.mjs +490 -0
- package/src/html-template.mjs +425 -355
- package/src/mcp.mjs +105 -14
- package/src/render.mjs +9 -0
package/src/html-template.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { esc, formatTokens } from "./helpers.mjs";
|
|
2
|
-
import { QUICK_REFERENCE } from "./constants.mjs";
|
|
2
|
+
import { QUICK_REFERENCE, VERSION, REPO_URL } from "./constants.mjs";
|
|
3
3
|
import {
|
|
4
4
|
renderCmd,
|
|
5
5
|
renderRule,
|
|
@@ -34,6 +34,335 @@ export function generateDashboardHtml({
|
|
|
34
34
|
mcpCount,
|
|
35
35
|
scanScope,
|
|
36
36
|
}) {
|
|
37
|
+
// ── Build tab content sections ──────────────────────────────────────────
|
|
38
|
+
|
|
39
|
+
// Skills card
|
|
40
|
+
const skillsHtml = globalSkills.length
|
|
41
|
+
? (() => {
|
|
42
|
+
const groups = groupSkillsByCategory(globalSkills);
|
|
43
|
+
const categoryHtml = Object.entries(groups)
|
|
44
|
+
.map(
|
|
45
|
+
([cat, skills], idx) =>
|
|
46
|
+
`<details class="skill-category"${idx === 0 ? " open" : ""}>` +
|
|
47
|
+
`<summary class="skill-category-label">${esc(cat)} <span class="cat-n">${skills.length}</span></summary>` +
|
|
48
|
+
skills.map((s) => renderSkill(s)).join("\n ") +
|
|
49
|
+
`</details>`,
|
|
50
|
+
)
|
|
51
|
+
.join("\n ");
|
|
52
|
+
return `<div class="card">
|
|
53
|
+
<h2>Skills <span class="n">${globalSkills.length}</span></h2>
|
|
54
|
+
${categoryHtml}
|
|
55
|
+
</div>`;
|
|
56
|
+
})()
|
|
57
|
+
: "";
|
|
58
|
+
|
|
59
|
+
// MCP card
|
|
60
|
+
const mcpHtml = mcpSummary.length
|
|
61
|
+
? (() => {
|
|
62
|
+
const rows = mcpSummary
|
|
63
|
+
.map((s) => {
|
|
64
|
+
const disabledClass = s.disabledIn > 0 ? " mcp-disabled" : "";
|
|
65
|
+
const disabledHint =
|
|
66
|
+
s.disabledIn > 0
|
|
67
|
+
? `<span class="mcp-disabled-hint">disabled in ${s.disabledIn} project${s.disabledIn > 1 ? "s" : ""}</span>`
|
|
68
|
+
: "";
|
|
69
|
+
const scopeBadge = s.userLevel
|
|
70
|
+
? `<span class="badge mcp-global">global</span>`
|
|
71
|
+
: s.recentlyActive
|
|
72
|
+
? `<span class="badge mcp-recent">recent</span>`
|
|
73
|
+
: `<span class="badge mcp-project">project</span>`;
|
|
74
|
+
const typeBadge = `<span class="badge mcp-type">${esc(s.type)}</span>`;
|
|
75
|
+
const projects = s.projects.length
|
|
76
|
+
? `<span class="mcp-projects">${s.projects.map((p) => esc(p)).join(", ")}</span>`
|
|
77
|
+
: "";
|
|
78
|
+
return `<div class="mcp-row${disabledClass}"><span class="mcp-name">${esc(s.name)}</span>${scopeBadge}${typeBadge}${disabledHint}${projects}</div>`;
|
|
79
|
+
})
|
|
80
|
+
.join("\n ");
|
|
81
|
+
const promoteHtml = mcpPromotions.length
|
|
82
|
+
? mcpPromotions
|
|
83
|
+
.map(
|
|
84
|
+
(p) =>
|
|
85
|
+
`<div class="mcp-promote"><span class="mcp-name">${esc(p.name)}</span> installed in ${p.projects.length} projects → add to <code>~/.claude/mcp_config.json</code></div>`,
|
|
86
|
+
)
|
|
87
|
+
.join("\n ")
|
|
88
|
+
: "";
|
|
89
|
+
const formerHtml = formerMcpServers.length
|
|
90
|
+
? `<div class="label" style="margin-top:.75rem">Formerly Installed</div>
|
|
91
|
+
${formerMcpServers
|
|
92
|
+
.map((s) => {
|
|
93
|
+
const hint = s.projects.length
|
|
94
|
+
? `<span class="mcp-projects">${s.projects.map((p) => esc(p)).join(", ")}</span>`
|
|
95
|
+
: "";
|
|
96
|
+
return `<div class="mcp-row mcp-former"><span class="mcp-name">${esc(s.name)}</span><span class="badge mcp-former-badge">removed</span>${hint}</div>`;
|
|
97
|
+
})
|
|
98
|
+
.join("\n ")}`
|
|
99
|
+
: "";
|
|
100
|
+
return `<div class="card">
|
|
101
|
+
<h2>MCP Servers <span class="n">${mcpSummary.length}</span></h2>
|
|
102
|
+
${rows}
|
|
103
|
+
${promoteHtml}
|
|
104
|
+
${formerHtml}
|
|
105
|
+
</div>`;
|
|
106
|
+
})()
|
|
107
|
+
: "";
|
|
108
|
+
|
|
109
|
+
// Usage bar cards (tools, languages, errors)
|
|
110
|
+
const toolsHtml = usageAnalytics.topTools.length
|
|
111
|
+
? (() => {
|
|
112
|
+
const maxCount = usageAnalytics.topTools[0].count;
|
|
113
|
+
const rows = usageAnalytics.topTools
|
|
114
|
+
.map((t) => {
|
|
115
|
+
const pct = maxCount > 0 ? Math.round((t.count / maxCount) * 100) : 0;
|
|
116
|
+
return `<div class="usage-bar-row"><span class="usage-bar-label">${esc(t.name)}</span><div class="usage-bar-track"><div class="usage-bar-fill usage-bar-tool" style="width:${pct}%"></div></div><span class="usage-bar-count">${t.count.toLocaleString()}</span></div>`;
|
|
117
|
+
})
|
|
118
|
+
.join("\n ");
|
|
119
|
+
return `<div class="card">
|
|
120
|
+
<h2>Top Tools Used <span class="n">${usageAnalytics.topTools.length}</span></h2>
|
|
121
|
+
${rows}
|
|
122
|
+
</div>`;
|
|
123
|
+
})()
|
|
124
|
+
: "";
|
|
125
|
+
|
|
126
|
+
const langsHtml = usageAnalytics.topLanguages.length
|
|
127
|
+
? (() => {
|
|
128
|
+
const maxCount = usageAnalytics.topLanguages[0].count;
|
|
129
|
+
const rows = usageAnalytics.topLanguages
|
|
130
|
+
.map((l) => {
|
|
131
|
+
const pct = maxCount > 0 ? Math.round((l.count / maxCount) * 100) : 0;
|
|
132
|
+
return `<div class="usage-bar-row"><span class="usage-bar-label">${esc(l.name)}</span><div class="usage-bar-track"><div class="usage-bar-fill usage-bar-lang" style="width:${pct}%"></div></div><span class="usage-bar-count">${l.count.toLocaleString()}</span></div>`;
|
|
133
|
+
})
|
|
134
|
+
.join("\n ");
|
|
135
|
+
return `<div class="card">
|
|
136
|
+
<h2>Languages <span class="n">${usageAnalytics.topLanguages.length}</span></h2>
|
|
137
|
+
${rows}
|
|
138
|
+
</div>`;
|
|
139
|
+
})()
|
|
140
|
+
: "";
|
|
141
|
+
|
|
142
|
+
const errorsHtml = usageAnalytics.errorCategories.length
|
|
143
|
+
? (() => {
|
|
144
|
+
const maxCount = usageAnalytics.errorCategories[0].count;
|
|
145
|
+
const rows = usageAnalytics.errorCategories
|
|
146
|
+
.map((e) => {
|
|
147
|
+
const pct = maxCount > 0 ? Math.round((e.count / maxCount) * 100) : 0;
|
|
148
|
+
return `<div class="usage-bar-row"><span class="usage-bar-label">${esc(e.name)}</span><div class="usage-bar-track"><div class="usage-bar-fill usage-bar-error" style="width:${pct}%"></div></div><span class="usage-bar-count">${e.count.toLocaleString()}</span></div>`;
|
|
149
|
+
})
|
|
150
|
+
.join("\n ");
|
|
151
|
+
return `<div class="card">
|
|
152
|
+
<h2>Top Errors <span class="n">${usageAnalytics.errorCategories.length}</span></h2>
|
|
153
|
+
${rows}
|
|
154
|
+
</div>`;
|
|
155
|
+
})()
|
|
156
|
+
: "";
|
|
157
|
+
|
|
158
|
+
// Activity/heatmap/peak hours/model usage card
|
|
159
|
+
const activityHtml = (() => {
|
|
160
|
+
const dailyActivity = statsCache.dailyActivity || [];
|
|
161
|
+
const hourCounts = statsCache.hourCounts || {};
|
|
162
|
+
const modelUsage = statsCache.modelUsage || {};
|
|
163
|
+
const hasActivity = dailyActivity.length > 0;
|
|
164
|
+
const hasHours = Object.keys(hourCounts).length > 0;
|
|
165
|
+
const hasModels = Object.keys(modelUsage).length > 0;
|
|
166
|
+
|
|
167
|
+
if (!hasActivity && !hasHours && !hasModels && !ccusageData) return "";
|
|
168
|
+
|
|
169
|
+
let content = "";
|
|
170
|
+
|
|
171
|
+
if (hasActivity) {
|
|
172
|
+
const dateMap = new Map(dailyActivity.map((d) => [d.date, d.messageCount || 0]));
|
|
173
|
+
const dates = dailyActivity.map((d) => d.date).sort();
|
|
174
|
+
const lastDate = new Date(dates[dates.length - 1]);
|
|
175
|
+
const firstDate = new Date(lastDate);
|
|
176
|
+
firstDate.setDate(firstDate.getDate() - 364);
|
|
177
|
+
|
|
178
|
+
const nonZero = dailyActivity
|
|
179
|
+
.map((d) => d.messageCount || 0)
|
|
180
|
+
.filter((n) => n > 0)
|
|
181
|
+
.sort((a, b) => a - b);
|
|
182
|
+
const q1 = nonZero[Math.floor(nonZero.length * 0.25)] || 1;
|
|
183
|
+
const q2 = nonZero[Math.floor(nonZero.length * 0.5)] || 2;
|
|
184
|
+
const q3 = nonZero[Math.floor(nonZero.length * 0.75)] || 3;
|
|
185
|
+
|
|
186
|
+
function level(count) {
|
|
187
|
+
if (count === 0) return "";
|
|
188
|
+
if (count <= q1) return " l1";
|
|
189
|
+
if (count <= q2) return " l2";
|
|
190
|
+
if (count <= q3) return " l3";
|
|
191
|
+
return " l4";
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const start = new Date(firstDate);
|
|
195
|
+
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
|
|
196
|
+
|
|
197
|
+
const months = [];
|
|
198
|
+
let lastMonth = -1;
|
|
199
|
+
const cursor1 = new Date(start);
|
|
200
|
+
let weekIdx = 0;
|
|
201
|
+
while (cursor1 <= lastDate) {
|
|
202
|
+
if (cursor1.getUTCDay() === 0) {
|
|
203
|
+
const m = cursor1.getUTCMonth();
|
|
204
|
+
if (m !== lastMonth) {
|
|
205
|
+
months.push({
|
|
206
|
+
name: cursor1.toLocaleString("en", { month: "short", timeZone: "UTC" }),
|
|
207
|
+
week: weekIdx,
|
|
208
|
+
});
|
|
209
|
+
lastMonth = m;
|
|
210
|
+
}
|
|
211
|
+
weekIdx++;
|
|
212
|
+
}
|
|
213
|
+
cursor1.setUTCDate(cursor1.getUTCDate() + 1);
|
|
214
|
+
}
|
|
215
|
+
const totalWeeks = weekIdx;
|
|
216
|
+
const monthLabels = months
|
|
217
|
+
.map((m) => {
|
|
218
|
+
const left = totalWeeks > 0 ? Math.round((m.week / totalWeeks) * 100) : 0;
|
|
219
|
+
return `<span class="heatmap-month" style="position:absolute;left:${left}%">${m.name}</span>`;
|
|
220
|
+
})
|
|
221
|
+
.join("");
|
|
222
|
+
|
|
223
|
+
let cells = "";
|
|
224
|
+
const cursor2 = new Date(start);
|
|
225
|
+
while (cursor2 <= lastDate) {
|
|
226
|
+
const key = cursor2.toISOString().slice(0, 10);
|
|
227
|
+
const count = dateMap.get(key) || 0;
|
|
228
|
+
cells += `<div class="heatmap-cell${level(count)}" title="${esc(key)}: ${count} messages"></div>`;
|
|
229
|
+
cursor2.setUTCDate(cursor2.getUTCDate() + 1);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
content += `<div class="label">Activity</div>
|
|
233
|
+
<div style="position:relative;margin-bottom:.5rem">
|
|
234
|
+
<div class="heatmap-months" style="position:relative;height:.8rem">${monthLabels}</div>
|
|
235
|
+
<div style="overflow-x:auto"><div class="heatmap">${cells}</div></div>
|
|
236
|
+
</div>`;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (hasHours) {
|
|
240
|
+
const maxHour = Math.max(...Object.values(hourCounts), 1);
|
|
241
|
+
let bars = "";
|
|
242
|
+
let labels = "";
|
|
243
|
+
for (let h = 0; h < 24; h++) {
|
|
244
|
+
const count = hourCounts[String(h)] || 0;
|
|
245
|
+
const pct = Math.round((count / maxHour) * 100);
|
|
246
|
+
bars += `<div class="peak-bar" style="height:${Math.max(pct, 2)}%" title="${esc(String(h))}:00 — ${count} messages"></div>`;
|
|
247
|
+
labels += `<div class="peak-label">${h % 6 === 0 ? h : ""}</div>`;
|
|
248
|
+
}
|
|
249
|
+
content += `<div class="label" style="margin-top:.75rem">Peak Hours</div>
|
|
250
|
+
<div class="peak-hours">${bars}</div>
|
|
251
|
+
<div class="peak-labels">${labels}</div>`;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (ccusageData) {
|
|
255
|
+
const modelCosts = {};
|
|
256
|
+
for (const day of ccusageData.daily) {
|
|
257
|
+
for (const mb of day.modelBreakdowns || []) {
|
|
258
|
+
if (!modelCosts[mb.modelName]) modelCosts[mb.modelName] = { cost: 0, tokens: 0 };
|
|
259
|
+
modelCosts[mb.modelName].cost += mb.cost || 0;
|
|
260
|
+
modelCosts[mb.modelName].tokens +=
|
|
261
|
+
(mb.inputTokens || 0) +
|
|
262
|
+
(mb.outputTokens || 0) +
|
|
263
|
+
(mb.cacheCreationTokens || 0) +
|
|
264
|
+
(mb.cacheReadTokens || 0);
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const modelRows = Object.entries(modelCosts)
|
|
268
|
+
.sort((a, b) => b[1].cost - a[1].cost)
|
|
269
|
+
.map(
|
|
270
|
+
([name, data]) =>
|
|
271
|
+
`<div class="model-row"><span class="model-name">${esc(name)}</span><span class="model-tokens">$${Math.round(data.cost).toLocaleString()} · ${formatTokens(data.tokens)}</span></div>`,
|
|
272
|
+
)
|
|
273
|
+
.join("\n ");
|
|
274
|
+
|
|
275
|
+
const t = ccusageData.totals;
|
|
276
|
+
const breakdownHtml = `<div class="token-breakdown">
|
|
277
|
+
<div class="tb-row"><span class="tb-label">Cache Read</span><span class="tb-val">${formatTokens(t.cacheReadTokens)}</span></div>
|
|
278
|
+
<div class="tb-row"><span class="tb-label">Cache Creation</span><span class="tb-val">${formatTokens(t.cacheCreationTokens)}</span></div>
|
|
279
|
+
<div class="tb-row"><span class="tb-label">Output</span><span class="tb-val">${formatTokens(t.outputTokens)}</span></div>
|
|
280
|
+
<div class="tb-row"><span class="tb-label">Input</span><span class="tb-val">${formatTokens(t.inputTokens)}</span></div>
|
|
281
|
+
</div>`;
|
|
282
|
+
|
|
283
|
+
content += `<div class="label" style="margin-top:.75rem">Model Usage (via ccusage)</div>
|
|
284
|
+
${modelRows}
|
|
285
|
+
<div class="label" style="margin-top:.75rem">Token Breakdown</div>
|
|
286
|
+
${breakdownHtml}`;
|
|
287
|
+
} else if (hasModels) {
|
|
288
|
+
const modelRows = Object.entries(modelUsage)
|
|
289
|
+
.map(([name, usage]) => {
|
|
290
|
+
const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
|
|
291
|
+
return { name, total };
|
|
292
|
+
})
|
|
293
|
+
.sort((a, b) => b.total - a.total)
|
|
294
|
+
.map(
|
|
295
|
+
(m) =>
|
|
296
|
+
`<div class="model-row"><span class="model-name">${esc(m.name)}</span><span class="model-tokens">${formatTokens(m.total)}</span></div>`,
|
|
297
|
+
)
|
|
298
|
+
.join("\n ");
|
|
299
|
+
content += `<div class="label" style="margin-top:.75rem">Model Usage (partial — install ccusage for full data)</div>
|
|
300
|
+
${modelRows}`;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
return `<div class="card">
|
|
304
|
+
<h2>Activity</h2>
|
|
305
|
+
${content}
|
|
306
|
+
</div>`;
|
|
307
|
+
})();
|
|
308
|
+
|
|
309
|
+
// Chains
|
|
310
|
+
const chainsHtml = chains.length
|
|
311
|
+
? `<div class="card">
|
|
312
|
+
<h2>Dependency Chains</h2>
|
|
313
|
+
${chains.map((c) => `<div class="chain">${c.nodes.map((n, i) => `<span class="chain-node">${esc(n.trim())}</span>${i < c.nodes.length - 1 ? `<span class="chain-arrow">${c.arrow}</span>` : ""}`).join("")}</div>`).join("\n ")}
|
|
314
|
+
</div>`
|
|
315
|
+
: "";
|
|
316
|
+
|
|
317
|
+
// Consolidation
|
|
318
|
+
const consolidationHtml = consolidationGroups.length
|
|
319
|
+
? `<div class="card">
|
|
320
|
+
<h2>Consolidation Opportunities <span class="n">${consolidationGroups.length}</span></h2>
|
|
321
|
+
${consolidationGroups.map((g) => `<div class="consolidation-hint"><span class="consolidation-stack">${esc(g.stack)}</span> <span class="consolidation-text">${esc(g.suggestion)}</span></div>`).join("\n ")}
|
|
322
|
+
</div>`
|
|
323
|
+
: "";
|
|
324
|
+
|
|
325
|
+
// Unconfigured repos
|
|
326
|
+
const unconfiguredHtml = unconfigured.length
|
|
327
|
+
? `<details class="card">
|
|
328
|
+
<summary style="cursor:pointer;list-style:none"><h2 style="margin:0">Unconfigured Repos <span class="n">${unconfiguredCount}</span></h2></summary>
|
|
329
|
+
<div style="margin-top:.75rem">
|
|
330
|
+
<div class="unconfigured-grid">
|
|
331
|
+
${unconfigured
|
|
332
|
+
.map((r) => {
|
|
333
|
+
const stackTag =
|
|
334
|
+
r.techStack && r.techStack.length
|
|
335
|
+
? `<span class="stack-tag">${esc(r.techStack.join(", "))}</span>`
|
|
336
|
+
: "";
|
|
337
|
+
const suggestionsHtml =
|
|
338
|
+
r.suggestions && r.suggestions.length
|
|
339
|
+
? `<div class="suggestion-hints">${r.suggestions.map((s) => `<span class="suggestion-hint">${esc(s)}</span>`).join("")}</div>`
|
|
340
|
+
: "";
|
|
341
|
+
return `<div class="unconfigured-item">${esc(r.name)}${stackTag}<span class="upath">${esc(r.shortPath)}</span>${suggestionsHtml}</div>`;
|
|
342
|
+
})
|
|
343
|
+
.join("\n ")}
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
</details>`
|
|
347
|
+
: "";
|
|
348
|
+
|
|
349
|
+
// Quick reference
|
|
350
|
+
const referenceHtml = `<div class="card">
|
|
351
|
+
<h2>Quick Reference</h2>
|
|
352
|
+
<div class="ref-grid">
|
|
353
|
+
<div class="ref-col">
|
|
354
|
+
<div class="label">Essential Commands</div>
|
|
355
|
+
${QUICK_REFERENCE.essentialCommands.map((c) => `<div class="ref-row"><code class="ref-cmd">${esc(c.cmd)}</code><span class="ref-desc">${esc(c.desc)}</span></div>`).join("\n ")}
|
|
356
|
+
</div>
|
|
357
|
+
<div class="ref-col">
|
|
358
|
+
<div class="label">Built-in Tools</div>
|
|
359
|
+
${QUICK_REFERENCE.tools.map((t) => `<div class="ref-row"><code class="ref-cmd">${esc(t.name)}</code><span class="ref-desc">${esc(t.desc)}</span></div>`).join("\n ")}
|
|
360
|
+
<div class="label" style="margin-top:.75rem">Keyboard Shortcuts</div>
|
|
361
|
+
${QUICK_REFERENCE.shortcuts.map((s) => `<div class="ref-row"><kbd class="ref-key">${esc(s.keys)}</kbd><span class="ref-desc">${esc(s.desc)}</span></div>`).join("\n ")}
|
|
362
|
+
</div>
|
|
363
|
+
</div>
|
|
364
|
+
</div>`;
|
|
365
|
+
|
|
37
366
|
return `<!DOCTYPE html>
|
|
38
367
|
<html lang="en">
|
|
39
368
|
<head>
|
|
@@ -61,14 +390,28 @@ export function generateDashboardHtml({
|
|
|
61
390
|
}
|
|
62
391
|
code, .cmd-name { font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace; }
|
|
63
392
|
h1 { font-size: 1.4rem; font-weight: 600; color: var(--accent); margin-bottom: .2rem; }
|
|
64
|
-
.sub { color: var(--text-dim); font-size: .78rem; margin-bottom:
|
|
393
|
+
.sub { color: var(--text-dim); font-size: .78rem; margin-bottom: 1rem; }
|
|
65
394
|
kbd { background: var(--surface2); border: 1px solid var(--border); border-radius: 3px; padding: .05rem .3rem; font-size: .7rem; font-family: inherit; }
|
|
66
395
|
|
|
396
|
+
/* ── Tabs ─────────────────────────────────────────────────── */
|
|
397
|
+
.tab-nav { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; overflow-x: auto; }
|
|
398
|
+
.tab-btn {
|
|
399
|
+
padding: .6rem 1.2rem; font-size: .78rem; font-weight: 500; color: var(--text-dim);
|
|
400
|
+
background: none; border: none; border-bottom: 2px solid transparent;
|
|
401
|
+
cursor: pointer; white-space: nowrap; font-family: inherit; transition: color .15s, border-color .15s;
|
|
402
|
+
}
|
|
403
|
+
.tab-btn:hover { color: var(--text); }
|
|
404
|
+
.tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
|
|
405
|
+
.tab-content { display: none; }
|
|
406
|
+
.tab-content.active { display: block; }
|
|
407
|
+
|
|
408
|
+
/* ── Cards ────────────────────────────────────────────────── */
|
|
67
409
|
.top-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 1.25rem; }
|
|
410
|
+
.top-grid > .card { margin-bottom: 0; }
|
|
68
411
|
@media (max-width: 900px) { .top-grid { grid-template-columns: 1fr; } }
|
|
69
412
|
|
|
70
|
-
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem; overflow: hidden; }
|
|
71
|
-
.card
|
|
413
|
+
.card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem; overflow: hidden; margin-bottom: 1.25rem; }
|
|
414
|
+
.card:last-child { margin-bottom: 0; }
|
|
72
415
|
.card h2 { font-size: .7rem; font-weight: 700; text-transform: uppercase; letter-spacing: .08em; color: var(--text-dim); margin-bottom: .75rem; display: flex; align-items: center; gap: .5rem; }
|
|
73
416
|
.card h2 .n { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: .05rem .35rem; font-size: .65rem; color: var(--accent); }
|
|
74
417
|
|
|
@@ -136,6 +479,7 @@ export function generateDashboardHtml({
|
|
|
136
479
|
.repo-card > summary {
|
|
137
480
|
cursor: pointer; list-style: none; padding: .85rem 1rem;
|
|
138
481
|
display: flex; flex-direction: column; gap: .3rem;
|
|
482
|
+
min-height: 7.5rem; justify-content: center;
|
|
139
483
|
}
|
|
140
484
|
.repo-card > summary::-webkit-details-marker { display: none; }
|
|
141
485
|
.repo-card > summary:hover { background: var(--surface2); }
|
|
@@ -175,6 +519,7 @@ export function generateDashboardHtml({
|
|
|
175
519
|
.mcp-projects { font-size: .65rem; color: var(--text-dim); margin-left: auto; }
|
|
176
520
|
.badge.mcp-global { color: var(--green); border-color: #4ade8033; background: #4ade8010; }
|
|
177
521
|
.badge.mcp-project { color: var(--blue); border-color: #60a5fa33; background: #60a5fa10; }
|
|
522
|
+
.badge.mcp-recent { color: var(--yellow); border-color: #fbbf2433; background: #fbbf2410; }
|
|
178
523
|
.badge.mcp-type { color: var(--text-dim); border-color: var(--border); background: var(--surface2); text-transform: none; font-size: .5rem; }
|
|
179
524
|
.mcp-promote { font-size: .72rem; color: var(--text-dim); padding: .4rem .5rem; background: rgba(251,191,36,.05); border: 1px solid rgba(251,191,36,.15); border-radius: 6px; margin-top: .3rem; }
|
|
180
525
|
.mcp-promote .mcp-name { color: var(--yellow); }
|
|
@@ -191,8 +536,8 @@ export function generateDashboardHtml({
|
|
|
191
536
|
.usage-bar-error { background: linear-gradient(90deg, var(--red), var(--yellow)); }
|
|
192
537
|
.usage-bar-count { font-size: .65rem; color: var(--text-dim); min-width: 40px; text-align: right; font-variant-numeric: tabular-nums; }
|
|
193
538
|
|
|
194
|
-
.heatmap { display: grid; grid-template-rows: repeat(7,
|
|
195
|
-
.heatmap-cell {
|
|
539
|
+
.heatmap { display: grid; grid-template-rows: repeat(7, 10px); grid-auto-flow: column; grid-auto-columns: 10px; gap: 3px; overflow-x: auto; width: fit-content; max-width: 100%; }
|
|
540
|
+
.heatmap-cell { border-radius: 2px; background: var(--surface2); width: 10px; height: 10px; }
|
|
196
541
|
.heatmap-cell.l1 { background: #0e4429; }
|
|
197
542
|
.heatmap-cell.l2 { background: #006d32; }
|
|
198
543
|
.heatmap-cell.l3 { background: #26a641; }
|
|
@@ -231,6 +576,9 @@ export function generateDashboardHtml({
|
|
|
231
576
|
.quick-win { font-size: .6rem; padding: .15rem .4rem; border-radius: 3px; background: rgba(251,191,36,.08); border: 1px solid rgba(251,191,36,.2); color: var(--yellow); }
|
|
232
577
|
.matched-skills { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .5rem; }
|
|
233
578
|
.matched-skill { font-size: .6rem; padding: .12rem .4rem; border-radius: 3px; background: rgba(251,191,36,.08); border: 1px solid rgba(251,191,36,.2); color: var(--yellow); font-family: 'SF Mono', monospace; }
|
|
579
|
+
.similar-repos { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .5rem; }
|
|
580
|
+
.similar-repo { font-size: .6rem; padding: .12rem .4rem; border-radius: 3px; background: rgba(99,179,237,.08); border: 1px solid rgba(99,179,237,.2); color: var(--blue); font-family: 'SF Mono', monospace; }
|
|
581
|
+
.similar-repo small { opacity: .6; }
|
|
234
582
|
.consolidation-hint { padding: .45rem .6rem; background: var(--surface2); border-radius: 6px; margin-top: .4rem; display: flex; align-items: baseline; gap: .5rem; }
|
|
235
583
|
.consolidation-hint:first-child { margin-top: 0; }
|
|
236
584
|
.consolidation-stack { font-size: .7rem; font-weight: 600; color: var(--accent); white-space: nowrap; }
|
|
@@ -296,7 +644,7 @@ export function generateDashboardHtml({
|
|
|
296
644
|
<body>
|
|
297
645
|
<h1>claude code dashboard</h1>
|
|
298
646
|
<button id="theme-toggle" class="theme-toggle" title="Toggle light/dark mode" aria-label="Toggle theme"><span class="theme-icon"></span></button>
|
|
299
|
-
<p class="sub">generated ${timestamp} · run <code>claude-code-dashboard</code> to refresh ·
|
|
647
|
+
<p class="sub">generated ${timestamp} · run <code>claude-code-dashboard</code> to refresh · <a href="${esc(REPO_URL)}" target="_blank" rel="noopener" style="color:var(--accent);text-decoration:none">v${esc(VERSION)}</a></p>
|
|
300
648
|
|
|
301
649
|
<div class="stats">
|
|
302
650
|
<div class="stat coverage"><b>${coveragePct}%</b><span>Coverage (${configuredCount}/${totalRepos})</span></div>
|
|
@@ -311,371 +659,88 @@ export function generateDashboardHtml({
|
|
|
311
659
|
${usageAnalytics.heavySessions > 0 ? `<div class="stat"><b>${usageAnalytics.heavySessions}</b><span>Heavy Sessions</span></div>` : ""}
|
|
312
660
|
</div>
|
|
313
661
|
|
|
314
|
-
<
|
|
315
|
-
<
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
</
|
|
319
|
-
<
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
`<summary class="skill-category-label">${esc(cat)} <span class="cat-n">${skills.length}</span></summary>` +
|
|
332
|
-
skills.map((s) => renderSkill(s)).join("\n ") +
|
|
333
|
-
`</details>`,
|
|
334
|
-
)
|
|
335
|
-
.join("\n ");
|
|
336
|
-
return `<div class="card full">
|
|
337
|
-
<h2>Skills <span class="n">${globalSkills.length}</span></h2>
|
|
338
|
-
${categoryHtml}
|
|
339
|
-
</div>`;
|
|
340
|
-
})()
|
|
341
|
-
: ""
|
|
342
|
-
}
|
|
343
|
-
${
|
|
344
|
-
mcpSummary.length
|
|
345
|
-
? (() => {
|
|
346
|
-
const rows = mcpSummary
|
|
347
|
-
.map((s) => {
|
|
348
|
-
const disabledClass = s.disabledIn > 0 ? " mcp-disabled" : "";
|
|
349
|
-
const disabledHint =
|
|
350
|
-
s.disabledIn > 0
|
|
351
|
-
? `<span class="mcp-disabled-hint">disabled in ${s.disabledIn} project${s.disabledIn > 1 ? "s" : ""}</span>`
|
|
352
|
-
: "";
|
|
353
|
-
const scopeBadge = s.userLevel
|
|
354
|
-
? `<span class="badge mcp-global">global</span>`
|
|
355
|
-
: `<span class="badge mcp-project">project</span>`;
|
|
356
|
-
const typeBadge = `<span class="badge mcp-type">${esc(s.type)}</span>`;
|
|
357
|
-
const projects = s.projects.length
|
|
358
|
-
? `<span class="mcp-projects">${s.projects.map((p) => esc(p)).join(", ")}</span>`
|
|
359
|
-
: "";
|
|
360
|
-
return `<div class="mcp-row${disabledClass}"><span class="mcp-name">${esc(s.name)}</span>${scopeBadge}${typeBadge}${disabledHint}${projects}</div>`;
|
|
361
|
-
})
|
|
362
|
-
.join("\n ");
|
|
363
|
-
const promoteHtml = mcpPromotions.length
|
|
364
|
-
? mcpPromotions
|
|
365
|
-
.map(
|
|
366
|
-
(p) =>
|
|
367
|
-
`<div class="mcp-promote"><span class="mcp-name">${esc(p.name)}</span> installed in ${p.projects.length} projects → add to <code>~/.claude/mcp_config.json</code></div>`,
|
|
368
|
-
)
|
|
369
|
-
.join("\n ")
|
|
370
|
-
: "";
|
|
371
|
-
const formerHtml = formerMcpServers.length
|
|
372
|
-
? `<div class="label" style="margin-top:.75rem">Formerly Installed</div>
|
|
373
|
-
${formerMcpServers.map((name) => `<div class="mcp-row mcp-former"><span class="mcp-name">${esc(name)}</span><span class="badge mcp-former-badge">removed</span></div>`).join("\n ")}`
|
|
374
|
-
: "";
|
|
375
|
-
return `<div class="card full">
|
|
376
|
-
<h2>MCP Servers <span class="n">${mcpSummary.length}</span></h2>
|
|
377
|
-
${rows}
|
|
378
|
-
${promoteHtml}
|
|
379
|
-
${formerHtml}
|
|
380
|
-
</div>`;
|
|
381
|
-
})()
|
|
382
|
-
: ""
|
|
383
|
-
}
|
|
384
|
-
${
|
|
385
|
-
usageAnalytics.topTools.length
|
|
386
|
-
? (() => {
|
|
387
|
-
const maxCount = usageAnalytics.topTools[0].count;
|
|
388
|
-
const rows = usageAnalytics.topTools
|
|
389
|
-
.map((t) => {
|
|
390
|
-
const pct = maxCount > 0 ? Math.round((t.count / maxCount) * 100) : 0;
|
|
391
|
-
return `<div class="usage-bar-row"><span class="usage-bar-label">${esc(t.name)}</span><div class="usage-bar-track"><div class="usage-bar-fill usage-bar-tool" style="width:${pct}%"></div></div><span class="usage-bar-count">${t.count.toLocaleString()}</span></div>`;
|
|
392
|
-
})
|
|
393
|
-
.join("\n ");
|
|
394
|
-
return `<div class="card">
|
|
395
|
-
<h2>Top Tools Used <span class="n">${usageAnalytics.topTools.length}</span></h2>
|
|
396
|
-
${rows}
|
|
397
|
-
</div>`;
|
|
398
|
-
})()
|
|
399
|
-
: ""
|
|
400
|
-
}
|
|
401
|
-
${
|
|
402
|
-
usageAnalytics.topLanguages.length
|
|
403
|
-
? (() => {
|
|
404
|
-
const maxCount = usageAnalytics.topLanguages[0].count;
|
|
405
|
-
const rows = usageAnalytics.topLanguages
|
|
406
|
-
.map((l) => {
|
|
407
|
-
const pct = maxCount > 0 ? Math.round((l.count / maxCount) * 100) : 0;
|
|
408
|
-
return `<div class="usage-bar-row"><span class="usage-bar-label">${esc(l.name)}</span><div class="usage-bar-track"><div class="usage-bar-fill usage-bar-lang" style="width:${pct}%"></div></div><span class="usage-bar-count">${l.count.toLocaleString()}</span></div>`;
|
|
409
|
-
})
|
|
410
|
-
.join("\n ");
|
|
411
|
-
return `<div class="card">
|
|
412
|
-
<h2>Languages <span class="n">${usageAnalytics.topLanguages.length}</span></h2>
|
|
413
|
-
${rows}
|
|
414
|
-
</div>`;
|
|
415
|
-
})()
|
|
416
|
-
: ""
|
|
417
|
-
}
|
|
418
|
-
${
|
|
419
|
-
usageAnalytics.errorCategories.length
|
|
420
|
-
? (() => {
|
|
421
|
-
const maxCount = usageAnalytics.errorCategories[0].count;
|
|
422
|
-
const rows = usageAnalytics.errorCategories
|
|
423
|
-
.map((e) => {
|
|
424
|
-
const pct = maxCount > 0 ? Math.round((e.count / maxCount) * 100) : 0;
|
|
425
|
-
return `<div class="usage-bar-row"><span class="usage-bar-label">${esc(e.name)}</span><div class="usage-bar-track"><div class="usage-bar-fill usage-bar-error" style="width:${pct}%"></div></div><span class="usage-bar-count">${e.count.toLocaleString()}</span></div>`;
|
|
426
|
-
})
|
|
427
|
-
.join("\n ");
|
|
428
|
-
return `<div class="card">
|
|
429
|
-
<h2>Top Errors <span class="n">${usageAnalytics.errorCategories.length}</span></h2>
|
|
430
|
-
${rows}
|
|
431
|
-
</div>`;
|
|
432
|
-
})()
|
|
433
|
-
: ""
|
|
434
|
-
}
|
|
435
|
-
${(() => {
|
|
436
|
-
const dailyActivity = statsCache.dailyActivity || [];
|
|
437
|
-
const hourCounts = statsCache.hourCounts || {};
|
|
438
|
-
const modelUsage = statsCache.modelUsage || {};
|
|
439
|
-
const hasActivity = dailyActivity.length > 0;
|
|
440
|
-
const hasHours = Object.keys(hourCounts).length > 0;
|
|
441
|
-
const hasModels = Object.keys(modelUsage).length > 0;
|
|
442
|
-
|
|
443
|
-
if (!hasActivity && !hasHours && !hasModels && !ccusageData) return "";
|
|
444
|
-
|
|
445
|
-
let content = "";
|
|
446
|
-
|
|
447
|
-
// Activity heatmap
|
|
448
|
-
if (hasActivity) {
|
|
449
|
-
const dateMap = new Map(dailyActivity.map((d) => [d.date, d.messageCount || 0]));
|
|
450
|
-
const dates = dailyActivity.map((d) => d.date).sort();
|
|
451
|
-
const lastDate = new Date(dates[dates.length - 1]);
|
|
452
|
-
const earliest = new Date(lastDate);
|
|
453
|
-
earliest.setDate(earliest.getDate() - 364);
|
|
454
|
-
const firstDate = new Date(dates[0]) < earliest ? earliest : new Date(dates[0]);
|
|
455
|
-
|
|
456
|
-
const nonZero = dailyActivity
|
|
457
|
-
.map((d) => d.messageCount || 0)
|
|
458
|
-
.filter((n) => n > 0)
|
|
459
|
-
.sort((a, b) => a - b);
|
|
460
|
-
const q1 = nonZero[Math.floor(nonZero.length * 0.25)] || 1;
|
|
461
|
-
const q2 = nonZero[Math.floor(nonZero.length * 0.5)] || 2;
|
|
462
|
-
const q3 = nonZero[Math.floor(nonZero.length * 0.75)] || 3;
|
|
463
|
-
|
|
464
|
-
function level(count) {
|
|
465
|
-
if (count === 0) return "";
|
|
466
|
-
if (count <= q1) return " l1";
|
|
467
|
-
if (count <= q2) return " l2";
|
|
468
|
-
if (count <= q3) return " l3";
|
|
469
|
-
return " l4";
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
const start = new Date(firstDate);
|
|
473
|
-
start.setUTCDate(start.getUTCDate() - start.getUTCDay());
|
|
474
|
-
|
|
475
|
-
const months = [];
|
|
476
|
-
let lastMonth = -1;
|
|
477
|
-
const cursor1 = new Date(start);
|
|
478
|
-
let weekIdx = 0;
|
|
479
|
-
while (cursor1 <= lastDate) {
|
|
480
|
-
if (cursor1.getUTCDay() === 0) {
|
|
481
|
-
const m = cursor1.getUTCMonth();
|
|
482
|
-
if (m !== lastMonth) {
|
|
483
|
-
months.push({
|
|
484
|
-
name: cursor1.toLocaleString("en", { month: "short", timeZone: "UTC" }),
|
|
485
|
-
week: weekIdx,
|
|
486
|
-
});
|
|
487
|
-
lastMonth = m;
|
|
488
|
-
}
|
|
489
|
-
weekIdx++;
|
|
490
|
-
}
|
|
491
|
-
cursor1.setUTCDate(cursor1.getUTCDate() + 1);
|
|
492
|
-
}
|
|
493
|
-
const totalWeeks = weekIdx;
|
|
494
|
-
const monthLabels = months
|
|
495
|
-
.map((m) => {
|
|
496
|
-
const left = totalWeeks > 0 ? Math.round((m.week / totalWeeks) * 100) : 0;
|
|
497
|
-
return `<span class="heatmap-month" style="position:absolute;left:${left}%">${m.name}</span>`;
|
|
498
|
-
})
|
|
499
|
-
.join("");
|
|
500
|
-
|
|
501
|
-
let cells = "";
|
|
502
|
-
const cursor2 = new Date(start);
|
|
503
|
-
while (cursor2 <= lastDate) {
|
|
504
|
-
const key = cursor2.toISOString().slice(0, 10);
|
|
505
|
-
const count = dateMap.get(key) || 0;
|
|
506
|
-
cells += `<div class="heatmap-cell${level(count)}" title="${esc(key)}: ${count} messages"></div>`;
|
|
507
|
-
cursor2.setUTCDate(cursor2.getUTCDate() + 1);
|
|
508
|
-
}
|
|
509
|
-
|
|
510
|
-
content += `<div class="label">Activity</div>
|
|
511
|
-
<div style="position:relative;margin-bottom:.5rem">
|
|
512
|
-
<div class="heatmap-months" style="position:relative;height:.8rem">${monthLabels}</div>
|
|
513
|
-
<div style="overflow-x:auto"><div class="heatmap">${cells}</div></div>
|
|
514
|
-
</div>`;
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
// Peak hours
|
|
518
|
-
if (hasHours) {
|
|
519
|
-
const maxHour = Math.max(...Object.values(hourCounts), 1);
|
|
520
|
-
let bars = "";
|
|
521
|
-
let labels = "";
|
|
522
|
-
for (let h = 0; h < 24; h++) {
|
|
523
|
-
const count = hourCounts[String(h)] || 0;
|
|
524
|
-
const pct = Math.round((count / maxHour) * 100);
|
|
525
|
-
bars += `<div class="peak-bar" style="height:${Math.max(pct, 2)}%" title="${esc(String(h))}:00 — ${count} messages"></div>`;
|
|
526
|
-
labels += `<div class="peak-label">${h % 6 === 0 ? h : ""}</div>`;
|
|
527
|
-
}
|
|
528
|
-
content += `<div class="label" style="margin-top:.75rem">Peak Hours</div>
|
|
529
|
-
<div class="peak-hours">${bars}</div>
|
|
530
|
-
<div class="peak-labels">${labels}</div>`;
|
|
531
|
-
}
|
|
532
|
-
|
|
533
|
-
// Model usage
|
|
534
|
-
if (ccusageData) {
|
|
535
|
-
const modelCosts = {};
|
|
536
|
-
for (const day of ccusageData.daily) {
|
|
537
|
-
for (const mb of day.modelBreakdowns || []) {
|
|
538
|
-
if (!modelCosts[mb.modelName]) modelCosts[mb.modelName] = { cost: 0, tokens: 0 };
|
|
539
|
-
modelCosts[mb.modelName].cost += mb.cost || 0;
|
|
540
|
-
modelCosts[mb.modelName].tokens +=
|
|
541
|
-
(mb.inputTokens || 0) +
|
|
542
|
-
(mb.outputTokens || 0) +
|
|
543
|
-
(mb.cacheCreationTokens || 0) +
|
|
544
|
-
(mb.cacheReadTokens || 0);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
const modelRows = Object.entries(modelCosts)
|
|
548
|
-
.sort((a, b) => b[1].cost - a[1].cost)
|
|
549
|
-
.map(
|
|
550
|
-
([name, data]) =>
|
|
551
|
-
`<div class="model-row"><span class="model-name">${esc(name)}</span><span class="model-tokens">$${Math.round(data.cost).toLocaleString()} · ${formatTokens(data.tokens)}</span></div>`,
|
|
552
|
-
)
|
|
553
|
-
.join("\n ");
|
|
554
|
-
|
|
555
|
-
const t = ccusageData.totals;
|
|
556
|
-
const breakdownHtml = `<div class="token-breakdown">
|
|
557
|
-
<div class="tb-row"><span class="tb-label">Cache Read</span><span class="tb-val">${formatTokens(t.cacheReadTokens)}</span></div>
|
|
558
|
-
<div class="tb-row"><span class="tb-label">Cache Creation</span><span class="tb-val">${formatTokens(t.cacheCreationTokens)}</span></div>
|
|
559
|
-
<div class="tb-row"><span class="tb-label">Output</span><span class="tb-val">${formatTokens(t.outputTokens)}</span></div>
|
|
560
|
-
<div class="tb-row"><span class="tb-label">Input</span><span class="tb-val">${formatTokens(t.inputTokens)}</span></div>
|
|
561
|
-
</div>`;
|
|
562
|
-
|
|
563
|
-
content += `<div class="label" style="margin-top:.75rem">Model Usage (via ccusage)</div>
|
|
564
|
-
${modelRows}
|
|
565
|
-
<div class="label" style="margin-top:.75rem">Token Breakdown</div>
|
|
566
|
-
${breakdownHtml}`;
|
|
567
|
-
} else if (hasModels) {
|
|
568
|
-
const modelRows = Object.entries(modelUsage)
|
|
569
|
-
.map(([name, usage]) => {
|
|
570
|
-
const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
|
|
571
|
-
return { name, total };
|
|
572
|
-
})
|
|
573
|
-
.sort((a, b) => b.total - a.total)
|
|
574
|
-
.map(
|
|
575
|
-
(m) =>
|
|
576
|
-
`<div class="model-row"><span class="model-name">${esc(m.name)}</span><span class="model-tokens">${formatTokens(m.total)}</span></div>`,
|
|
577
|
-
)
|
|
578
|
-
.join("\n ");
|
|
579
|
-
content += `<div class="label" style="margin-top:.75rem">Model Usage (partial — install ccusage for full data)</div>
|
|
580
|
-
${modelRows}`;
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
return `<div class="card full">
|
|
584
|
-
<h2>Activity</h2>
|
|
585
|
-
${content}
|
|
586
|
-
</div>`;
|
|
587
|
-
})()}
|
|
588
|
-
<details class="card full">
|
|
589
|
-
<summary style="cursor:pointer;list-style:none"><h2 style="margin:0">Quick Reference</h2></summary>
|
|
590
|
-
<div style="margin-top:.75rem">
|
|
591
|
-
<div class="ref-grid">
|
|
592
|
-
<div class="ref-col">
|
|
593
|
-
<div class="label">Essential Commands</div>
|
|
594
|
-
${QUICK_REFERENCE.essentialCommands.map((c) => `<div class="ref-row"><code class="ref-cmd">${esc(c.cmd)}</code><span class="ref-desc">${esc(c.desc)}</span></div>`).join("\n ")}
|
|
595
|
-
</div>
|
|
596
|
-
<div class="ref-col">
|
|
597
|
-
<div class="label">Built-in Tools</div>
|
|
598
|
-
${QUICK_REFERENCE.tools.map((t) => `<div class="ref-row"><code class="ref-cmd">${esc(t.name)}</code><span class="ref-desc">${esc(t.desc)}</span></div>`).join("\n ")}
|
|
599
|
-
<div class="label" style="margin-top:.75rem">Keyboard Shortcuts</div>
|
|
600
|
-
${QUICK_REFERENCE.shortcuts.map((s) => `<div class="ref-row"><kbd class="ref-key">${esc(s.keys)}</kbd><span class="ref-desc">${esc(s.desc)}</span></div>`).join("\n ")}
|
|
601
|
-
</div>
|
|
662
|
+
<nav class="tab-nav">
|
|
663
|
+
<button class="tab-btn active" data-tab="overview">Overview</button>
|
|
664
|
+
<button class="tab-btn" data-tab="skills-mcp">Skills & MCP</button>
|
|
665
|
+
<button class="tab-btn" data-tab="analytics">Analytics</button>
|
|
666
|
+
<button class="tab-btn" data-tab="repos">Repos</button>
|
|
667
|
+
<button class="tab-btn" data-tab="reference">Reference</button>
|
|
668
|
+
</nav>
|
|
669
|
+
|
|
670
|
+
<div class="tab-content active" id="tab-overview">
|
|
671
|
+
<div class="top-grid">
|
|
672
|
+
<div class="card" style="margin-bottom:0">
|
|
673
|
+
<h2>Global Commands <span class="n">${globalCmds.length}</span></h2>
|
|
674
|
+
${globalCmds.map((c) => renderCmd(c)).join("\n ")}
|
|
675
|
+
</div>
|
|
676
|
+
<div class="card" style="margin-bottom:0">
|
|
677
|
+
<h2>Global Rules <span class="n">${globalRules.length}</span></h2>
|
|
678
|
+
${globalRules.map((r) => renderRule(r)).join("\n ")}
|
|
602
679
|
</div>
|
|
603
680
|
</div>
|
|
604
|
-
|
|
605
|
-
${
|
|
606
|
-
chains.length
|
|
607
|
-
? `<div class="card full">
|
|
608
|
-
<h2>Dependency Chains</h2>
|
|
609
|
-
${chains.map((c) => `<div class="chain">${c.nodes.map((n, i) => `<span class="chain-node">${esc(n.trim())}</span>${i < c.nodes.length - 1 ? `<span class="chain-arrow">${c.arrow}</span>` : ""}`).join("")}</div>`).join("\n ")}
|
|
610
|
-
</div>`
|
|
611
|
-
: ""
|
|
612
|
-
}
|
|
681
|
+
${chainsHtml}
|
|
682
|
+
${consolidationHtml}
|
|
613
683
|
</div>
|
|
614
684
|
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
<h2>Consolidation Opportunities <span class="n">${consolidationGroups.length}</span></h2>
|
|
619
|
-
${consolidationGroups.map((g) => `<div class="consolidation-hint"><span class="consolidation-stack">${esc(g.stack)}</span> <span class="consolidation-text">${esc(g.suggestion)}</span></div>`).join("\n ")}
|
|
620
|
-
</div>`
|
|
621
|
-
: ""
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
<div class="search-bar">
|
|
625
|
-
<input type="text" id="search" placeholder="search repos..." autocomplete="off">
|
|
626
|
-
<span class="search-hint"><kbd>/</kbd></span>
|
|
627
|
-
</div>
|
|
628
|
-
<div class="group-controls">
|
|
629
|
-
<label class="group-label">Group by:</label>
|
|
630
|
-
<select id="group-by" class="group-select">
|
|
631
|
-
<option value="none">None</option>
|
|
632
|
-
<option value="stack">Tech Stack</option>
|
|
633
|
-
<option value="parent">Parent Directory</option>
|
|
634
|
-
</select>
|
|
685
|
+
<div class="tab-content" id="tab-skills-mcp">
|
|
686
|
+
${skillsHtml}
|
|
687
|
+
${mcpHtml}
|
|
635
688
|
</div>
|
|
636
689
|
|
|
637
|
-
<div class="
|
|
638
|
-
|
|
690
|
+
<div class="tab-content" id="tab-analytics">
|
|
691
|
+
<div class="top-grid">
|
|
692
|
+
${toolsHtml || ""}
|
|
693
|
+
${langsHtml || ""}
|
|
694
|
+
</div>
|
|
695
|
+
${errorsHtml}
|
|
696
|
+
${activityHtml}
|
|
639
697
|
</div>
|
|
640
698
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
<div style="margin-top:.75rem">
|
|
646
|
-
<div class="unconfigured-grid">
|
|
647
|
-
${unconfigured
|
|
648
|
-
.map((r) => {
|
|
649
|
-
const stackTag =
|
|
650
|
-
r.techStack && r.techStack.length
|
|
651
|
-
? `<span class="stack-tag">${esc(r.techStack.join(", "))}</span>`
|
|
652
|
-
: "";
|
|
653
|
-
const suggestionsHtml =
|
|
654
|
-
r.suggestions && r.suggestions.length
|
|
655
|
-
? `<div class="suggestion-hints">${r.suggestions.map((s) => `<span class="suggestion-hint">${esc(s)}</span>`).join("")}</div>`
|
|
656
|
-
: "";
|
|
657
|
-
return `<div class="unconfigured-item">${esc(r.name)}${stackTag}<span class="upath">${esc(r.shortPath)}</span>${suggestionsHtml}</div>`;
|
|
658
|
-
})
|
|
659
|
-
.join("\n ")}
|
|
660
|
-
</div>
|
|
699
|
+
<div class="tab-content" id="tab-repos">
|
|
700
|
+
<div class="search-bar">
|
|
701
|
+
<input type="text" id="search" placeholder="search repos..." autocomplete="off">
|
|
702
|
+
<span class="search-hint"><kbd>/</kbd></span>
|
|
661
703
|
</div>
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
704
|
+
<div class="group-controls">
|
|
705
|
+
<label class="group-label">Group by:</label>
|
|
706
|
+
<select id="group-by" class="group-select">
|
|
707
|
+
<option value="none">None</option>
|
|
708
|
+
<option value="stack">Tech Stack</option>
|
|
709
|
+
<option value="parent">Parent Directory</option>
|
|
710
|
+
</select>
|
|
711
|
+
</div>
|
|
712
|
+
<div class="repo-grid" id="repo-grid">
|
|
713
|
+
${configured.map((r) => renderRepoCard(r)).join("\n")}
|
|
714
|
+
</div>
|
|
715
|
+
${unconfiguredHtml}
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
<div class="tab-content" id="tab-reference">
|
|
719
|
+
${referenceHtml}
|
|
720
|
+
</div>
|
|
665
721
|
|
|
666
722
|
<div class="ts">found ${totalRepos} repos · ${configuredCount} configured · ${unconfiguredCount} unconfigured · scanned ${scanScope} · ${timestamp}</div>
|
|
667
723
|
|
|
668
724
|
<script>
|
|
669
|
-
|
|
670
|
-
|
|
725
|
+
document.querySelectorAll('.tab-btn').forEach(function(btn) {
|
|
726
|
+
btn.addEventListener('click', function() {
|
|
727
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
728
|
+
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
729
|
+
btn.classList.add('active');
|
|
730
|
+
document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
|
|
731
|
+
});
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
var input = document.getElementById('search');
|
|
735
|
+
var hint = document.querySelector('.search-hint');
|
|
671
736
|
|
|
672
737
|
input.addEventListener('input', function(e) {
|
|
673
|
-
|
|
738
|
+
var q = e.target.value.toLowerCase();
|
|
674
739
|
hint.style.display = q ? 'none' : '';
|
|
675
740
|
document.querySelectorAll('.repo-card').forEach(function(card) {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
741
|
+
var name = card.dataset.name.toLowerCase();
|
|
742
|
+
var path = (card.dataset.path || '').toLowerCase();
|
|
743
|
+
var text = card.textContent.toLowerCase();
|
|
679
744
|
card.style.display = (q === '' || name.includes(q) || path.includes(q) || text.includes(q)) ? '' : 'none';
|
|
680
745
|
});
|
|
681
746
|
});
|
|
@@ -683,6 +748,11 @@ input.addEventListener('input', function(e) {
|
|
|
683
748
|
document.addEventListener('keydown', function(e) {
|
|
684
749
|
if (e.key === '/' && document.activeElement !== input) {
|
|
685
750
|
e.preventDefault();
|
|
751
|
+
// Switch to repos tab first
|
|
752
|
+
document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
|
|
753
|
+
document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
|
|
754
|
+
document.querySelector('[data-tab="repos"]').classList.add('active');
|
|
755
|
+
document.getElementById('tab-repos').classList.add('active');
|
|
686
756
|
input.focus();
|
|
687
757
|
}
|
|
688
758
|
if (e.key === 'Escape' && document.activeElement === input) {
|