@viren/claude-code-dashboard 0.0.4 → 0.0.5

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.
@@ -1,946 +0,0 @@
1
- import { esc, formatTokens } from "./helpers.mjs";
2
- import { QUICK_REFERENCE, VERSION, REPO_URL } from "./constants.mjs";
3
- import {
4
- renderCmd,
5
- renderRule,
6
- renderSkill,
7
- renderRepoCard,
8
- groupSkillsByCategory,
9
- healthScoreColor,
10
- } from "./render.mjs";
11
-
12
- export function generateDashboardHtml({
13
- configured,
14
- unconfigured,
15
- globalCmds,
16
- globalRules,
17
- globalSkills,
18
- chains,
19
- mcpSummary,
20
- mcpPromotions,
21
- formerMcpServers,
22
- consolidationGroups,
23
- usageAnalytics,
24
- ccusageData,
25
- statsCache,
26
- timestamp,
27
- coveragePct,
28
- totalRepos,
29
- configuredCount,
30
- unconfiguredCount,
31
- totalRepoCmds,
32
- avgHealth,
33
- driftCount,
34
- mcpCount,
35
- scanScope,
36
- insights,
37
- insightsReport,
38
- }) {
39
- // ── Build tab content sections ──────────────────────────────────────────
40
-
41
- // Skills card
42
- const skillsHtml = globalSkills.length
43
- ? (() => {
44
- const groups = groupSkillsByCategory(globalSkills);
45
- const categoryHtml = Object.entries(groups)
46
- .map(
47
- ([cat, skills], idx) =>
48
- `<details class="skill-category"${idx === 0 ? " open" : ""}>` +
49
- `<summary class="skill-category-label">${esc(cat)} <span class="cat-n">${skills.length}</span></summary>` +
50
- skills.map((s) => renderSkill(s)).join("\n ") +
51
- `</details>`,
52
- )
53
- .join("\n ");
54
- return `<div class="card" id="section-skills">
55
- <h2>Skills <span class="n">${globalSkills.length}</span></h2>
56
- ${categoryHtml}
57
- </div>`;
58
- })()
59
- : "";
60
-
61
- // MCP card
62
- const mcpHtml = mcpSummary.length
63
- ? (() => {
64
- const rows = mcpSummary
65
- .map((s) => {
66
- const disabledClass = s.disabledIn > 0 ? " mcp-disabled" : "";
67
- const disabledHint =
68
- s.disabledIn > 0
69
- ? `<span class="mcp-disabled-hint">disabled in ${s.disabledIn} project${s.disabledIn > 1 ? "s" : ""}</span>`
70
- : "";
71
- const scopeBadge = s.userLevel
72
- ? `<span class="badge mcp-global">global</span>`
73
- : s.recentlyActive
74
- ? `<span class="badge mcp-recent">recent</span>`
75
- : `<span class="badge mcp-project">project</span>`;
76
- const typeBadge = `<span class="badge mcp-type">${esc(s.type)}</span>`;
77
- const projects =
78
- !s.userLevel && s.projects.length
79
- ? `<span class="mcp-projects">${s.projects.map((p) => esc(p)).join(", ")}</span>`
80
- : "";
81
- return `<div class="mcp-row${disabledClass}"><span class="mcp-name">${esc(s.name)}</span>${scopeBadge}${typeBadge}${disabledHint}${projects}</div>`;
82
- })
83
- .join("\n ");
84
- const promoteHtml = mcpPromotions.length
85
- ? mcpPromotions
86
- .map(
87
- (p) =>
88
- `<div class="mcp-promote"><span class="mcp-name">${esc(p.name)}</span> installed in ${p.projects.length} projects &rarr; add to <code>~/.claude/mcp_config.json</code></div>`,
89
- )
90
- .join("\n ")
91
- : "";
92
- const formerHtml = formerMcpServers.length
93
- ? `<div class="label" style="margin-top:.75rem">Formerly Installed</div>
94
- ${formerMcpServers
95
- .map((s) => {
96
- const hint = s.projects.length
97
- ? `<span class="mcp-projects">${s.projects.map((p) => esc(p)).join(", ")}</span>`
98
- : "";
99
- 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>`;
100
- })
101
- .join("\n ")}`
102
- : "";
103
- return `<div class="card" id="section-mcp">
104
- <h2>MCP Servers <span class="n">${mcpSummary.length}</span></h2>
105
- ${rows}
106
- ${promoteHtml}
107
- ${formerHtml}
108
- </div>`;
109
- })()
110
- : "";
111
-
112
- // Usage bar cards (tools, languages, errors)
113
- const toolsHtml = usageAnalytics.topTools.length
114
- ? (() => {
115
- const maxCount = usageAnalytics.topTools[0].count;
116
- const rows = usageAnalytics.topTools
117
- .map((t) => {
118
- const pct = maxCount > 0 ? Math.round((t.count / maxCount) * 100) : 0;
119
- 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>`;
120
- })
121
- .join("\n ");
122
- return `<div class="card">
123
- <h2>Top Tools Used <span class="n">${usageAnalytics.topTools.length}</span></h2>
124
- ${rows}
125
- </div>`;
126
- })()
127
- : "";
128
-
129
- const langsHtml = usageAnalytics.topLanguages.length
130
- ? (() => {
131
- const maxCount = usageAnalytics.topLanguages[0].count;
132
- const rows = usageAnalytics.topLanguages
133
- .map((l) => {
134
- const pct = maxCount > 0 ? Math.round((l.count / maxCount) * 100) : 0;
135
- 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>`;
136
- })
137
- .join("\n ");
138
- return `<div class="card">
139
- <h2>Languages <span class="n">${usageAnalytics.topLanguages.length}</span></h2>
140
- ${rows}
141
- </div>`;
142
- })()
143
- : "";
144
-
145
- const errorsHtml = usageAnalytics.errorCategories.length
146
- ? (() => {
147
- const maxCount = usageAnalytics.errorCategories[0].count;
148
- const rows = usageAnalytics.errorCategories
149
- .map((e) => {
150
- const pct = maxCount > 0 ? Math.round((e.count / maxCount) * 100) : 0;
151
- 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>`;
152
- })
153
- .join("\n ");
154
- return `<div class="card">
155
- <h2>Top Errors <span class="n">${usageAnalytics.errorCategories.length}</span></h2>
156
- ${rows}
157
- </div>`;
158
- })()
159
- : "";
160
-
161
- // Activity/heatmap/peak hours/model usage card
162
- const activityHtml = (() => {
163
- const dailyActivity = statsCache.dailyActivity || [];
164
- const hourCounts = statsCache.hourCounts || {};
165
- const modelUsage = statsCache.modelUsage || {};
166
- const hasActivity = dailyActivity.length > 0;
167
- const hasHours = Object.keys(hourCounts).length > 0;
168
- const hasModels = Object.keys(modelUsage).length > 0;
169
-
170
- if (!hasActivity && !hasHours && !hasModels && !ccusageData) return "";
171
-
172
- let content = "";
173
-
174
- if (hasActivity) {
175
- const dateMap = new Map(dailyActivity.map((d) => [d.date, d.messageCount || 0]));
176
- const dates = dailyActivity.map((d) => d.date).sort();
177
- const lastDate = new Date(dates[dates.length - 1]);
178
- const firstDate = new Date(lastDate);
179
- firstDate.setDate(firstDate.getDate() - 364);
180
-
181
- const nonZero = dailyActivity
182
- .map((d) => d.messageCount || 0)
183
- .filter((n) => n > 0)
184
- .sort((a, b) => a - b);
185
- const q1 = nonZero[Math.floor(nonZero.length * 0.25)] || 1;
186
- const q2 = nonZero[Math.floor(nonZero.length * 0.5)] || 2;
187
- const q3 = nonZero[Math.floor(nonZero.length * 0.75)] || 3;
188
-
189
- function level(count) {
190
- if (count === 0) return "";
191
- if (count <= q1) return " l1";
192
- if (count <= q2) return " l2";
193
- if (count <= q3) return " l3";
194
- return " l4";
195
- }
196
-
197
- const start = new Date(firstDate);
198
- start.setUTCDate(start.getUTCDate() - start.getUTCDay());
199
-
200
- const months = [];
201
- let lastMonth = -1;
202
- const cursor1 = new Date(start);
203
- let weekIdx = 0;
204
- while (cursor1 <= lastDate) {
205
- if (cursor1.getUTCDay() === 0) {
206
- const m = cursor1.getUTCMonth();
207
- if (m !== lastMonth) {
208
- months.push({
209
- name: cursor1.toLocaleString("en", { month: "short", timeZone: "UTC" }),
210
- week: weekIdx,
211
- });
212
- lastMonth = m;
213
- }
214
- weekIdx++;
215
- }
216
- cursor1.setUTCDate(cursor1.getUTCDate() + 1);
217
- }
218
- const totalWeeks = weekIdx;
219
- const monthLabels = months
220
- .map((m) => {
221
- const left = totalWeeks > 0 ? Math.round((m.week / totalWeeks) * 100) : 0;
222
- return `<span class="heatmap-month" style="position:absolute;left:${left}%">${m.name}</span>`;
223
- })
224
- .join("");
225
-
226
- let cells = "";
227
- const cursor2 = new Date(start);
228
- while (cursor2 <= lastDate) {
229
- const key = cursor2.toISOString().slice(0, 10);
230
- const count = dateMap.get(key) || 0;
231
- const fmtDate = cursor2.toLocaleDateString("en-US", {
232
- month: "short",
233
- day: "numeric",
234
- year: "numeric",
235
- });
236
- cells += `<div class="heatmap-cell${level(count)}" title="${esc(fmtDate)}: ${count} messages"></div>`;
237
- cursor2.setUTCDate(cursor2.getUTCDate() + 1);
238
- }
239
-
240
- content += `<div class="label">Activity</div>
241
- <div style="overflow-x:auto;margin-bottom:.5rem">
242
- <div style="width:fit-content;position:relative">
243
- <div class="heatmap-months" style="position:relative;height:.8rem">${monthLabels}</div>
244
- <div class="heatmap">${cells}</div>
245
- </div>
246
- </div>`;
247
- }
248
-
249
- if (hasHours) {
250
- const maxHour = Math.max(...Object.values(hourCounts), 1);
251
- let bars = "";
252
- let labels = "";
253
- for (let h = 0; h < 24; h++) {
254
- const count = hourCounts[String(h)] || 0;
255
- const pct = Math.round((count / maxHour) * 100);
256
- bars += `<div class="peak-bar" style="height:${Math.max(pct, 2)}%" title="${esc(String(h))}:00 — ${count} messages"></div>`;
257
- labels += `<div class="peak-label">${h % 6 === 0 ? h : ""}</div>`;
258
- }
259
- content += `<div class="label" style="margin-top:.75rem">Peak Hours</div>
260
- <div class="peak-hours">${bars}</div>
261
- <div class="peak-labels">${labels}</div>`;
262
- }
263
-
264
- if (ccusageData) {
265
- const modelCosts = {};
266
- for (const day of ccusageData.daily) {
267
- for (const mb of day.modelBreakdowns || []) {
268
- if (!modelCosts[mb.modelName]) modelCosts[mb.modelName] = { cost: 0, tokens: 0 };
269
- modelCosts[mb.modelName].cost += mb.cost || 0;
270
- modelCosts[mb.modelName].tokens +=
271
- (mb.inputTokens || 0) +
272
- (mb.outputTokens || 0) +
273
- (mb.cacheCreationTokens || 0) +
274
- (mb.cacheReadTokens || 0);
275
- }
276
- }
277
- const modelRows = Object.entries(modelCosts)
278
- .sort((a, b) => b[1].cost - a[1].cost)
279
- .map(
280
- ([name, data]) =>
281
- `<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>`,
282
- )
283
- .join("\n ");
284
-
285
- const t = ccusageData.totals;
286
- const breakdownHtml = `<div class="token-breakdown">
287
- <div class="tb-row"><span class="tb-label">Cache Read</span><span class="tb-val">${formatTokens(t.cacheReadTokens)}</span></div>
288
- <div class="tb-row"><span class="tb-label">Cache Creation</span><span class="tb-val">${formatTokens(t.cacheCreationTokens)}</span></div>
289
- <div class="tb-row"><span class="tb-label">Output</span><span class="tb-val">${formatTokens(t.outputTokens)}</span></div>
290
- <div class="tb-row"><span class="tb-label">Input</span><span class="tb-val">${formatTokens(t.inputTokens)}</span></div>
291
- </div>`;
292
-
293
- content += `<div class="label" style="margin-top:.75rem">Model Usage (via ccusage)</div>
294
- ${modelRows}
295
- <div class="label" style="margin-top:.75rem">Token Breakdown</div>
296
- ${breakdownHtml}`;
297
- } else if (hasModels) {
298
- const modelRows = Object.entries(modelUsage)
299
- .map(([name, usage]) => {
300
- const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
301
- return { name, total };
302
- })
303
- .sort((a, b) => b.total - a.total)
304
- .map(
305
- (m) =>
306
- `<div class="model-row"><span class="model-name">${esc(m.name)}</span><span class="model-tokens">${formatTokens(m.total)}</span></div>`,
307
- )
308
- .join("\n ");
309
- content += `<div class="label" style="margin-top:.75rem">Model Usage (partial — install ccusage for full data)</div>
310
- ${modelRows}`;
311
- }
312
-
313
- return `<div class="card" id="section-activity">
314
- <h2>Activity</h2>
315
- ${content}
316
- </div>`;
317
- })();
318
-
319
- // Chains
320
- const chainsHtml = chains.length
321
- ? `<div class="card">
322
- <h2>Dependency Chains</h2>
323
- ${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 ")}
324
- </div>`
325
- : "";
326
-
327
- // Consolidation
328
- const consolidationHtml = consolidationGroups.length
329
- ? `<div class="card">
330
- <h2>Consolidation Opportunities <span class="n">${consolidationGroups.length}</span></h2>
331
- ${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 ")}
332
- </div>`
333
- : "";
334
-
335
- // Unconfigured repos
336
- const unconfiguredHtml = unconfigured.length
337
- ? `<details class="card">
338
- <summary style="cursor:pointer;list-style:none"><h2 style="margin:0">Unconfigured Repos <span class="n">${unconfiguredCount}</span></h2></summary>
339
- <div style="margin-top:.75rem">
340
- <div class="unconfigured-grid">
341
- ${unconfigured
342
- .map((r) => {
343
- const stackTag =
344
- r.techStack && r.techStack.length
345
- ? `<span class="stack-tag">${esc(r.techStack.join(", "))}</span>`
346
- : "";
347
- const suggestionsHtml =
348
- r.suggestions && r.suggestions.length
349
- ? `<div class="suggestion-hints">${r.suggestions.map((s) => `<span class="suggestion-hint">${esc(s)}</span>`).join("")}</div>`
350
- : "";
351
- return `<div class="unconfigured-item">${esc(r.name)}${stackTag}<span class="upath">${esc(r.shortPath)}</span>${suggestionsHtml}</div>`;
352
- })
353
- .join("\n ")}
354
- </div>
355
- </div>
356
- </details>`
357
- : "";
358
-
359
- // Quick reference
360
- const referenceHtml = `<div class="card">
361
- <h2>Quick Reference</h2>
362
- <div class="ref-grid">
363
- <div class="ref-col">
364
- <div class="label">Essential Commands</div>
365
- ${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 ")}
366
- </div>
367
- <div class="ref-col">
368
- <div class="label">Built-in Tools</div>
369
- ${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 ")}
370
- <div class="label" style="margin-top:.75rem">Keyboard Shortcuts</div>
371
- ${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 ")}
372
- </div>
373
- </div>
374
- </div>`;
375
-
376
- return `<!DOCTYPE html>
377
- <html lang="en">
378
- <head>
379
- <meta charset="utf-8">
380
- <meta name="viewport" content="width=device-width, initial-scale=1">
381
- <title>Claude Code Dashboard</title>
382
- <style>
383
- :root {
384
- --bg: #0a0a0a; --surface: #111; --surface2: #1a1a1a; --border: #262626;
385
- --text: #e5e5e5; --text-dim: #777; --accent: #c4956a; --accent-dim: #8b6a4a;
386
- --green: #4ade80; --blue: #60a5fa; --purple: #a78bfa; --yellow: #fbbf24;
387
- --red: #f87171;
388
- }
389
- [data-theme="light"] {
390
- --bg: #f5f5f5; --surface: #fff; --surface2: #f0f0f0; --border: #e0e0e0;
391
- --text: #1a1a1a; --text-dim: #666; --accent: #9b6b47; --accent-dim: #b8956e;
392
- --green: #16a34a; --blue: #2563eb; --purple: #7c3aed; --yellow: #ca8a04;
393
- --red: #dc2626;
394
- }
395
- * { margin: 0; padding: 0; box-sizing: border-box; }
396
- body {
397
- font-family: -apple-system, BlinkMacSystemFont, 'SF Pro Text', sans-serif;
398
- background: var(--bg); color: var(--text);
399
- padding: 2.5rem 2rem; line-height: 1.5; max-width: 1200px; margin: 0 auto;
400
- }
401
- code, .cmd-name { font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace; }
402
- h1 { font-size: 1.4rem; font-weight: 600; color: var(--accent); margin-bottom: .2rem; }
403
- .sub { color: var(--text-dim); font-size: .78rem; margin-bottom: 1rem; }
404
- kbd { background: var(--surface2); border: 1px solid var(--border); border-radius: 3px; padding: .05rem .3rem; font-size: .7rem; font-family: inherit; }
405
-
406
- /* ── Tabs ─────────────────────────────────────────────────── */
407
- .tab-nav { display: flex; gap: 0; border-bottom: 1px solid var(--border); margin-bottom: 1.5rem; overflow-x: auto; }
408
- .tab-btn {
409
- padding: .6rem 1.2rem; font-size: .78rem; font-weight: 500; color: var(--text-dim);
410
- background: none; border: none; border-bottom: 2px solid transparent;
411
- cursor: pointer; white-space: nowrap; font-family: inherit; transition: color .15s, border-color .15s;
412
- }
413
- .tab-btn:hover { color: var(--text); }
414
- .tab-btn.active { color: var(--accent); border-bottom-color: var(--accent); }
415
- .tab-content { display: none; }
416
- .tab-content.active { display: block; }
417
-
418
- /* ── Cards ────────────────────────────────────────────────── */
419
- .top-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 1.25rem; }
420
- .top-grid > .card { margin-bottom: 0; }
421
- @media (max-width: 900px) { .top-grid { grid-template-columns: 1fr; } }
422
-
423
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem; overflow: hidden; margin-bottom: 1.25rem; }
424
- .card:last-child { margin-bottom: 0; }
425
- .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; }
426
- .card h2 .n { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: .05rem .35rem; font-size: .65rem; color: var(--accent); }
427
-
428
- .cmd-row, details.cmd-detail > summary { display: flex; align-items: baseline; padding: .35rem .25rem; gap: .75rem; border-bottom: 1px solid var(--border); font-size: .82rem; }
429
- .cmd-row:last-child, details.cmd-detail:last-child:not([open]) > summary { border-bottom: none; }
430
- .cmd-name { font-weight: 600; color: var(--green); white-space: nowrap; font-size: .8rem; flex-shrink: 0; }
431
- .cmd-desc { color: var(--text-dim); font-size: .75rem; text-align: right; flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
432
-
433
- details.cmd-detail { border-bottom: 1px solid var(--border); }
434
- details.cmd-detail:last-child { border-bottom: none; }
435
- details.cmd-detail > summary { cursor: pointer; list-style: none; border-radius: 4px; transition: background .1s; }
436
- details.cmd-detail[open] > summary, details.cmd-detail > summary:hover { background: var(--surface2); }
437
- details.cmd-detail > summary::-webkit-details-marker { display: none; }
438
- .detail-body { padding: .6rem .5rem .6rem 1rem; background: var(--surface2); border-radius: 0 0 6px 6px; margin-bottom: .15rem; }
439
- .detail-section { color: var(--blue); font-size: .72rem; font-weight: 600; margin-top: .35rem; }
440
- .detail-section:first-child { margin-top: 0; }
441
- .detail-step, .detail-key { font-size: .7rem; padding: .1rem 0 .1rem .9rem; position: relative; }
442
- .detail-step { color: var(--text); }
443
- .detail-step::before { content: "\\2192"; position: absolute; left: 0; color: var(--accent-dim); font-size: .65rem; }
444
- .detail-key { color: var(--yellow); }
445
- .detail-key::before { content: "\\2022"; position: absolute; left: .15rem; color: var(--accent-dim); }
446
-
447
- .label { color: var(--text-dim); font-size: .65rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; margin: .85rem 0 .35rem; }
448
- .label:first-child { margin-top: 0; }
449
-
450
- .agent-section { border-bottom: 1px solid var(--border); }
451
- .agent-section:last-child { border-bottom: none; }
452
- .agent-section > summary { cursor: pointer; list-style: none; display: flex; align-items: baseline; padding: .3rem .25rem; font-size: .78rem; font-weight: 500; color: var(--text); border-radius: 4px; transition: background .1s; }
453
- .agent-section > summary::-webkit-details-marker { display: none; }
454
- .agent-section > summary:hover, .agent-section[open] > summary { background: var(--surface2); }
455
- .agent-section[open] > summary { color: var(--blue); }
456
- .agent-section-preview { padding: .3rem .4rem .5rem 1rem; background: var(--surface2); border-radius: 0 0 4px 4px; margin-bottom: .1rem; }
457
- .agent-section-preview .line { color: var(--text-dim); font-size: .68rem; line-height: 1.5; padding: .05rem 0; }
458
-
459
- .chain { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; padding: .65rem .75rem; background: var(--surface2); border-radius: 6px; margin-top: .4rem; }
460
- .chain:first-child { margin-top: 0; }
461
- .chain-node { background: var(--surface); border: 1px solid var(--accent-dim); border-radius: 5px; padding: .25rem .55rem; font-size: .75rem; font-weight: 500; color: var(--accent); }
462
- .chain-arrow { color: var(--text-dim); font-size: .85rem; }
463
-
464
- .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .65rem; margin-bottom: 1.5rem; }
465
- .stat { text-align: center; padding: .65rem .5rem; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; cursor: pointer; transition: border-color .15s, transform .1s; }
466
- .stat:hover { border-color: var(--accent-dim); transform: translateY(-1px); }
467
- .stat b { display: block; font-size: 1.4rem; color: var(--accent); }
468
- .stat span { font-size: .6rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .06em; }
469
- .stat.coverage b { color: ${coveragePct >= 70 ? "var(--green)" : coveragePct >= 40 ? "var(--yellow)" : "var(--red)"}; }
470
-
471
- .search-bar { margin-bottom: 1rem; position: relative; }
472
- .search-bar input {
473
- width: 100%; padding: .6rem .9rem; padding-right: 4rem; font-size: .82rem;
474
- background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
475
- color: var(--text); outline: none; transition: border-color .15s; font-family: inherit;
476
- }
477
- .search-bar input::placeholder { color: var(--text-dim); }
478
- .search-bar input:focus { border-color: var(--accent-dim); }
479
- .search-hint { position: absolute; right: .75rem; top: 50%; transform: translateY(-50%); pointer-events: none; }
480
-
481
- .repo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: .75rem; margin-bottom: 1.25rem; }
482
- @media (max-width: 1000px) { .repo-grid { grid-template-columns: 1fr 1fr; } }
483
- @media (max-width: 600px) { .repo-grid { grid-template-columns: 1fr; } }
484
-
485
- .repo-card {
486
- background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
487
- overflow: hidden; transition: border-color .15s;
488
- }
489
- .repo-card[open] { grid-column: 1 / -1; border-color: var(--accent-dim); }
490
- .repo-card > summary {
491
- cursor: pointer; list-style: none; padding: .85rem 1rem;
492
- display: flex; flex-direction: column; gap: .3rem;
493
- min-height: 7.5rem; justify-content: center;
494
- }
495
- .repo-card > summary::-webkit-details-marker { display: none; }
496
- .repo-card > summary:hover { background: var(--surface2); }
497
- .repo-header { display: flex; align-items: center; justify-content: space-between; }
498
- .repo-card .repo-name {
499
- font-size: .88rem; font-weight: 600; color: var(--text);
500
- display: flex; align-items: center; gap: .4rem;
501
- }
502
- .repo-card .repo-preview {
503
- font-size: .7rem; color: var(--text-dim); line-height: 1.4;
504
- display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden;
505
- }
506
- .repo-card .badges { display: flex; gap: .3rem; margin-top: .2rem; flex-wrap: wrap; }
507
- .badge {
508
- font-size: .58rem; font-weight: 600; text-transform: uppercase; letter-spacing: .04em;
509
- padding: .12rem .4rem; border-radius: 3px; border: 1px solid;
510
- }
511
- .badge.cmds { color: var(--green); border-color: #4ade8033; background: #4ade8010; }
512
- .badge.rules { color: var(--purple); border-color: #a78bfa33; background: #a78bfa10; }
513
- .badge.agent { color: var(--blue); border-color: #60a5fa33; background: #60a5fa10; }
514
- .badge.skills { color: var(--yellow); border-color: #fbbf2433; background: #fbbf2410; }
515
- .badge.source { font-size: .5rem; padding: .08rem .3rem; margin-left: .4rem; text-transform: none; letter-spacing: .02em; flex-shrink: 0; }
516
- .badge.source.superpowers { color: var(--purple); border-color: #a78bfa33; background: #a78bfa10; }
517
- .badge.source.skillssh { color: var(--blue); border-color: #60a5fa33; background: #60a5fa10; }
518
- .badge.source.custom { color: var(--text-dim); border-color: var(--border); background: var(--surface2); }
519
- .skill-name { color: var(--yellow) !important; }
520
- .skill-category { margin-top: .75rem; }
521
- .skill-category:first-child { margin-top: 0; }
522
- .skill-category-label { font-size: .6rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--text-dim); padding: .3rem 0; margin-bottom: .25rem; border-bottom: 1px solid var(--border); display: flex; align-items: center; gap: .4rem; }
523
- .skill-category-label .cat-n { font-size: .55rem; color: var(--accent-dim); }
524
-
525
- .mcp-row { display: flex; align-items: center; gap: .5rem; padding: .3rem .25rem; border-bottom: 1px solid var(--border); font-size: .8rem; flex-wrap: wrap; }
526
- .mcp-row:last-child { border-bottom: none; }
527
- .mcp-row.mcp-disabled { opacity: .5; }
528
- .mcp-disabled-hint { font-size: .6rem; color: var(--red); opacity: .8; }
529
- .mcp-name { font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace; font-weight: 600; color: var(--text); font-size: .78rem; }
530
- .mcp-projects { font-size: .65rem; color: var(--text-dim); margin-left: auto; }
531
- .badge.mcp-global { color: var(--green); border-color: #4ade8033; background: #4ade8010; }
532
- .badge.mcp-project { color: var(--blue); border-color: #60a5fa33; background: #60a5fa10; }
533
- .badge.mcp-recent { color: var(--yellow); border-color: #fbbf2433; background: #fbbf2410; }
534
- .badge.mcp-type { color: var(--text-dim); border-color: var(--border); background: var(--surface2); text-transform: none; font-size: .5rem; }
535
- .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; }
536
- .mcp-promote .mcp-name { color: var(--yellow); }
537
- .mcp-promote code { font-size: .65rem; color: var(--accent); }
538
-
539
- .insight-card { margin-bottom: 1.25rem; }
540
- .insight-row { display: flex; align-items: flex-start; gap: .6rem; padding: .5rem .6rem; border-radius: 6px; margin-bottom: .35rem; font-size: .78rem; line-height: 1.4; }
541
- .insight-row:last-child { margin-bottom: 0; }
542
- .insight-icon { flex-shrink: 0; font-size: .85rem; line-height: 1; margin-top: .1rem; }
543
- .insight-body { flex: 1; min-width: 0; }
544
- .insight-title { font-weight: 600; color: var(--text); }
545
- .insight-detail { color: var(--text-dim); font-size: .72rem; margin-top: .15rem; }
546
- .insight-action { color: var(--accent-dim); font-size: .68rem; font-style: italic; margin-top: .15rem; }
547
- .insight-row.warning { background: rgba(251,191,36,.06); border: 1px solid rgba(251,191,36,.15); }
548
- .insight-row.info { background: rgba(96,165,250,.06); border: 1px solid rgba(96,165,250,.15); }
549
- .insight-row.tip { background: rgba(74,222,128,.06); border: 1px solid rgba(74,222,128,.15); }
550
- .insight-row.promote { background: rgba(192,132,252,.06); border: 1px solid rgba(192,132,252,.15); }
551
-
552
- .report-card { margin-bottom: 1.25rem; }
553
- .report-subtitle { font-size: .72rem; color: var(--text-dim); margin-bottom: .75rem; }
554
- .report-stats { display: flex; flex-wrap: wrap; gap: .5rem; margin-bottom: .75rem; }
555
- .report-stat { background: var(--bg); border: 1px solid var(--border); border-radius: 6px; padding: .4rem .6rem; text-align: center; min-width: 70px; }
556
- .report-stat b { display: block; font-size: 1rem; color: var(--accent); }
557
- .report-stat span { font-size: .55rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .04em; }
558
- .report-glance { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .75rem; }
559
- .report-glance-item { font-size: .75rem; line-height: 1.5; color: var(--text-dim); padding: .5rem .6rem; background: var(--bg); border-radius: 6px; border: 1px solid var(--border); }
560
- .report-glance-item strong { color: var(--text); font-weight: 600; }
561
- .report-link { display: inline-block; margin-top: .5rem; font-size: .72rem; color: var(--accent); text-decoration: none; }
562
- .report-link:hover { text-decoration: underline; }
563
- .mcp-former { opacity: .4; }
564
- .badge.mcp-former-badge { color: var(--text-dim); border-color: var(--border); background: var(--surface2); font-style: italic; }
565
-
566
- .usage-bar-row { display: flex; align-items: center; gap: .5rem; padding: .25rem 0; font-size: .75rem; }
567
- .usage-bar-label { width: 100px; flex-shrink: 0; color: var(--text); font-weight: 500; font-size: .72rem; }
568
- .usage-bar-track { flex: 1; height: 8px; background: var(--surface2); border-radius: 4px; overflow: hidden; }
569
- .usage-bar-fill { height: 100%; border-radius: 4px; transition: width .3s; }
570
- .usage-bar-tool { background: linear-gradient(90deg, var(--blue), var(--green)); }
571
- .usage-bar-lang { background: linear-gradient(90deg, var(--green), var(--accent)); }
572
- .usage-bar-error { background: linear-gradient(90deg, var(--red), var(--yellow)); }
573
- .usage-bar-count { font-size: .65rem; color: var(--text-dim); min-width: 40px; text-align: right; font-variant-numeric: tabular-nums; }
574
-
575
- .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%; }
576
- .heatmap-cell { border-radius: 2px; background: var(--surface2); width: 10px; height: 10px; }
577
- .heatmap-cell.l1 { background: #0e4429; }
578
- .heatmap-cell.l2 { background: #006d32; }
579
- .heatmap-cell.l3 { background: #26a641; }
580
- .heatmap-cell.l4 { background: #39d353; }
581
- [data-theme="light"] .heatmap-cell.l1 { background: #9be9a8; }
582
- [data-theme="light"] .heatmap-cell.l2 { background: #40c463; }
583
- [data-theme="light"] .heatmap-cell.l3 { background: #30a14e; }
584
- [data-theme="light"] .heatmap-cell.l4 { background: #216e39; }
585
-
586
- .heatmap-months { display: flex; font-size: .5rem; color: var(--text-dim); margin-bottom: .2rem; }
587
- .heatmap-month { flex: 1; }
588
-
589
- .chart-tooltip { position: fixed; pointer-events: none; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px; padding: .3rem .5rem; font-size: .7rem; white-space: nowrap; z-index: 999; box-shadow: 0 2px 8px rgba(0,0,0,.25); opacity: 0; transition: opacity .1s; }
590
- .chart-tooltip.visible { opacity: 1; }
591
-
592
- .peak-hours { display: flex; align-items: flex-end; gap: 2px; height: 40px; }
593
- .peak-bar { flex: 1; background: var(--purple); border-radius: 2px 2px 0 0; min-width: 4px; opacity: .7; }
594
- .peak-labels { display: flex; gap: 2px; font-size: .45rem; color: var(--text-dim); }
595
- .peak-label { flex: 1; text-align: center; min-width: 4px; }
596
-
597
- .model-row { display: flex; justify-content: space-between; padding: .2rem 0; font-size: .72rem; border-bottom: 1px solid var(--border); }
598
- .model-row:last-child { border-bottom: none; }
599
- .model-name { color: var(--text); font-weight: 500; }
600
- .model-tokens { color: var(--text-dim); font-variant-numeric: tabular-nums; }
601
- .token-breakdown { margin-top: .25rem; }
602
- .tb-row { display: flex; justify-content: space-between; padding: .15rem 0; font-size: .68rem; }
603
- .tb-label { color: var(--text-dim); }
604
- .tb-val { color: var(--text); font-variant-numeric: tabular-nums; font-weight: 500; }
605
-
606
- .health-bar { height: 4px; background: var(--surface2); border-radius: 2px; margin: .4rem 0 .5rem; position: relative; overflow: hidden; }
607
- .health-fill { height: 100%; border-radius: 2px; transition: width .3s; }
608
- .health-label { position: absolute; right: 0; top: -14px; font-size: .55rem; color: var(--text-dim); }
609
- .badge.stack { color: var(--accent); border-color: var(--accent-dim); background: rgba(196,149,106,.08); text-transform: none; }
610
- .drift { font-size: .58rem; margin-left: .4rem; font-weight: 600; }
611
- .drift-low { color: var(--text-dim); }
612
- .drift-medium { color: var(--yellow); }
613
- .drift-high { color: var(--red); }
614
- .quick-wins { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .5rem; }
615
- .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); }
616
- .matched-skills { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .5rem; }
617
- .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; }
618
- .similar-repos { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .5rem; }
619
- .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; }
620
- .similar-repo small { opacity: .6; }
621
- .consolidation-hint { padding: .45rem .6rem; background: var(--surface2); border-radius: 6px; margin-top: .4rem; display: flex; align-items: baseline; gap: .5rem; }
622
- .consolidation-hint:first-child { margin-top: 0; }
623
- .consolidation-stack { font-size: .7rem; font-weight: 600; color: var(--accent); white-space: nowrap; }
624
- .consolidation-text { font-size: .7rem; color: var(--text-dim); }
625
-
626
- .unconfigured-item .stack-tag { font-size: .5rem; color: var(--accent-dim); margin-left: .3rem; }
627
-
628
- .freshness-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; }
629
- .freshness-dot.fresh { background: var(--green); }
630
- .freshness-dot.aging { background: var(--yellow); }
631
- .freshness-dot.stale { background: var(--red); }
632
-
633
- .repo-body { padding: 0 1rem 1rem; }
634
- .repo-meta { display: flex; justify-content: space-between; align-items: center; margin-bottom: .5rem; padding-bottom: .4rem; border-bottom: 1px solid var(--border); }
635
- .repo-path { font-size: .68rem; color: var(--text-dim); font-family: 'SF Mono', monospace; }
636
- .freshness { font-size: .65rem; font-weight: 500; }
637
- .freshness.fresh { color: var(--green); }
638
- .freshness.aging { color: var(--yellow); }
639
- .freshness.stale { color: var(--red); }
640
- .repo-desc { color: var(--text-dim); font-size: .75rem; line-height: 1.45; margin-bottom: .75rem; padding-bottom: .6rem; border-bottom: 1px solid var(--border); }
641
-
642
- .unconfigured-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: .4rem; }
643
- @media (max-width: 900px) { .unconfigured-grid { grid-template-columns: repeat(2, 1fr); } }
644
- .unconfigured-item { font-size: .72rem; padding: .3rem .5rem; border-radius: 4px; background: var(--surface2); color: var(--text-dim); }
645
- .unconfigured-item .upath { font-size: .6rem; color: #555; display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
646
- .suggestion-hints { display: flex; flex-wrap: wrap; gap: .2rem; margin-top: .25rem; }
647
- .suggestion-hint { font-size: .5rem; padding: .08rem .3rem; border-radius: 2px; background: rgba(96,165,250,.08); border: 1px solid rgba(96,165,250,.15); color: var(--blue); }
648
-
649
- .ts { text-align: center; color: var(--text-dim); font-size: .65rem; margin-top: 2rem; padding-top: 1rem; border-top: 1px solid var(--border); }
650
-
651
- .theme-toggle {
652
- position: fixed; top: 1rem; right: 1rem; z-index: 100;
653
- background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
654
- padding: .4rem .6rem; cursor: pointer; color: var(--text-dim); font-size: .75rem;
655
- transition: background .15s, border-color .15s;
656
- }
657
- .theme-toggle:hover { border-color: var(--accent-dim); }
658
- .theme-icon::before { content: "\\263E"; }
659
- [data-theme="light"] .theme-icon::before { content: "\\2600"; }
660
-
661
- .ref-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; }
662
- @media (max-width: 700px) { .ref-grid { grid-template-columns: 1fr; } }
663
- .ref-row { display: flex; align-items: baseline; gap: .5rem; padding: .2rem 0; font-size: .72rem; }
664
- .ref-cmd { font-size: .7rem; color: var(--green); white-space: nowrap; min-width: 100px; }
665
- .ref-key { min-width: 90px; font-size: .65rem; }
666
- .ref-desc { color: var(--text-dim); font-size: .68rem; }
667
-
668
- details.skill-category > summary { cursor: pointer; list-style: none; }
669
- details.skill-category > summary::-webkit-details-marker { display: none; }
670
- details.skill-category > summary:hover { color: var(--accent); }
671
- details.skill-category[open] > summary { color: var(--blue); }
672
-
673
- .group-controls { display: flex; align-items: center; gap: .5rem; margin-bottom: 1rem; }
674
- .group-label { font-size: .7rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .06em; }
675
- .group-select { font-size: .75rem; padding: .3rem .5rem; background: var(--surface); color: var(--text); border: 1px solid var(--border); border-radius: 6px; outline: none; font-family: inherit; }
676
- .group-select:focus { border-color: var(--accent-dim); }
677
- .group-heading { font-size: .75rem; font-weight: 700; text-transform: uppercase; letter-spacing: .06em; color: var(--accent); padding: .5rem 0 .25rem; margin-top: .75rem; border-bottom: 1px solid var(--border); grid-column: 1 / -1; }
678
-
679
- .repo-card[open] .repo-preview { display: none; }
680
- details.cmd-detail[open] .cmd-desc { white-space: normal; text-overflow: unset; overflow: visible; }
681
- </style>
682
- </head>
683
- <body>
684
- <h1>claude code dashboard</h1>
685
- <button id="theme-toggle" class="theme-toggle" title="Toggle light/dark mode" aria-label="Toggle theme"><span class="theme-icon"></span></button>
686
- <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>
687
-
688
- <div class="stats">
689
- <div class="stat coverage" data-nav="repos" data-section="repo-grid" title="View repos"><b>${coveragePct}%</b><span>Coverage (${configuredCount}/${totalRepos})</span></div>
690
- <div class="stat" data-nav="repos" data-section="repo-grid" title="View repos" style="${avgHealth >= 70 ? "border-color:#4ade8033" : avgHealth >= 40 ? "border-color:#fbbf2433" : "border-color:#f8717133"}"><b style="color:${healthScoreColor(avgHealth)}">${avgHealth}</b><span>Avg Health</span></div>
691
- <div class="stat" data-nav="overview" data-section="section-commands" title="View commands"><b>${globalCmds.length}</b><span>Global Commands</span></div>
692
- <div class="stat" data-nav="skills-mcp" data-section="section-skills" title="View skills"><b>${globalSkills.length}</b><span>Skills</span></div>
693
- <div class="stat" data-nav="repos" data-section="repo-grid" title="View repos"><b>${totalRepoCmds}</b><span>Repo Commands</span></div>
694
- ${mcpCount > 0 ? `<div class="stat" data-nav="skills-mcp" data-section="section-mcp" title="View MCP servers"><b>${mcpCount}</b><span>MCP Servers</span></div>` : ""}
695
- ${driftCount > 0 ? `<div class="stat" data-nav="repos" data-section="repo-grid" title="View drifting repos" style="border-color:#f8717133"><b style="color:var(--red)">${driftCount}</b><span>Drifting Repos</span></div>` : ""}
696
- ${ccusageData ? `<div class="stat" data-nav="analytics" data-section="section-activity" title="View analytics" style="border-color:#4ade8033"><b style="color:var(--green)">$${Math.round(Number(ccusageData.totals.totalCost) || 0).toLocaleString()}</b><span>Total Spent</span></div>` : ""}
697
- ${ccusageData ? `<div class="stat" data-nav="analytics" data-section="section-activity" title="View analytics"><b>${formatTokens(ccusageData.totals.totalTokens).replace(" tokens", "")}</b><span>Total Tokens</span></div>` : ""}
698
- ${usageAnalytics.heavySessions > 0 ? `<div class="stat" data-nav="analytics" data-section="section-activity" title="View analytics"><b>${usageAnalytics.heavySessions}</b><span>Heavy Sessions</span></div>` : ""}
699
- </div>
700
-
701
- <nav class="tab-nav">
702
- <button class="tab-btn active" data-tab="overview">Overview</button>
703
- <button class="tab-btn" data-tab="skills-mcp">Skills & MCP</button>
704
- <button class="tab-btn" data-tab="analytics">Analytics</button>
705
- <button class="tab-btn" data-tab="repos">Repos</button>
706
- <button class="tab-btn" data-tab="reference">Reference</button>
707
- </nav>
708
-
709
- <div class="tab-content active" id="tab-overview">
710
- <div class="top-grid">
711
- <div class="card" id="section-commands" style="margin-bottom:0">
712
- <h2>Global Commands <span class="n">${globalCmds.length}</span></h2>
713
- ${globalCmds.map((c) => renderCmd(c)).join("\n ")}
714
- </div>
715
- <div class="card" style="margin-bottom:0">
716
- <h2>Global Rules <span class="n">${globalRules.length}</span></h2>
717
- ${globalRules.map((r) => renderRule(r)).join("\n ")}
718
- </div>
719
- </div>
720
- ${
721
- insights && insights.length > 0
722
- ? `<div class="card insight-card">
723
- <h2>Insights <span class="n">${insights.length}</span></h2>
724
- ${insights
725
- .map(
726
- (i) =>
727
- `<div class="insight-row ${esc(i.type)}">
728
- <span class="insight-icon">${i.type === "warning" ? "&#9888;" : i.type === "tip" ? "&#10024;" : i.type === "promote" ? "&#8593;" : "&#9432;"}</span>
729
- <div class="insight-body">
730
- <div class="insight-title">${esc(i.title)}</div>
731
- ${i.detail ? `<div class="insight-detail">${esc(i.detail)}</div>` : ""}
732
- ${i.action ? `<div class="insight-action">${esc(i.action)}</div>` : ""}
733
- </div>
734
- </div>`,
735
- )
736
- .join("\n ")}
737
- </div>`
738
- : ""
739
- }
740
- ${chainsHtml}
741
- ${consolidationHtml}
742
- </div>
743
-
744
- <div class="tab-content" id="tab-skills-mcp">
745
- ${skillsHtml}
746
- ${mcpHtml}
747
- </div>
748
-
749
- <div class="tab-content" id="tab-analytics">
750
- ${
751
- insightsReport
752
- ? `<div class="card report-card" id="section-insights-report">
753
- <h2>Claude Code Insights</h2>
754
- ${insightsReport.subtitle ? `<div class="report-subtitle">${esc(insightsReport.subtitle)}</div>` : ""}
755
- ${
756
- insightsReport.stats.length > 0
757
- ? `<div class="report-stats">${insightsReport.stats
758
- .map((s) => {
759
- if (s.isDiff) {
760
- const parts = s.value.match(/^([+-][^/]+)\/([-+].+)$/);
761
- if (parts) {
762
- return `<div class="report-stat"><b><span style="color:var(--green)">${esc(parts[1])}</span><span style="color:var(--text-dim)">/</span><span style="color:var(--red)">${esc(parts[2])}</span></b><span>${esc(s.label)}</span></div>`;
763
- }
764
- }
765
- return `<div class="report-stat"><b>${esc(s.value)}</b><span>${esc(s.label)}</span></div>`;
766
- })
767
- .join("")}</div>`
768
- : ""
769
- }
770
- ${
771
- insightsReport.glance.length > 0
772
- ? `<div class="report-glance">${insightsReport.glance.map((g) => `<div class="report-glance-item"><strong>${esc(g.label)}:</strong> ${esc(g.text)}</div>`).join("")}</div>`
773
- : ""
774
- }
775
- <a class="report-link" href="file://${encodeURI(insightsReport.filePath)}" target="_blank">View full insights report &rarr;</a>
776
- </div>`
777
- : `<div class="card report-card">
778
- <h2>Claude Code Insights</h2>
779
- <div class="report-glance"><div class="report-glance-item">No insights report found. Run <code>/insights</code> in Claude Code to generate a personalized report with usage patterns, friction points, and feature suggestions.</div></div>
780
- </div>`
781
- }
782
- <div class="top-grid">
783
- ${toolsHtml || ""}
784
- ${langsHtml || ""}
785
- </div>
786
- ${errorsHtml}
787
- ${activityHtml}
788
- </div>
789
-
790
- <div class="tab-content" id="tab-repos">
791
- <div class="search-bar">
792
- <input type="text" id="search" placeholder="search repos..." autocomplete="off">
793
- <span class="search-hint"><kbd>/</kbd></span>
794
- </div>
795
- <div class="group-controls">
796
- <label class="group-label">Group by:</label>
797
- <select id="group-by" class="group-select">
798
- <option value="none">None</option>
799
- <option value="stack">Tech Stack</option>
800
- <option value="parent">Parent Directory</option>
801
- </select>
802
- </div>
803
- <div class="repo-grid" id="repo-grid">
804
- ${configured.map((r) => renderRepoCard(r)).join("\n")}
805
- </div>
806
- ${unconfiguredHtml}
807
- </div>
808
-
809
- <div class="tab-content" id="tab-reference">
810
- ${referenceHtml}
811
- </div>
812
-
813
- <div class="ts">found ${totalRepos} repos · ${configuredCount} configured · ${unconfiguredCount} unconfigured · scanned ${scanScope} · ${timestamp}</div>
814
-
815
- <div class="chart-tooltip" id="chart-tooltip"></div>
816
- <script>
817
- function switchTab(tabName) {
818
- document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
819
- document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
820
- var btn = document.querySelector('.tab-btn[data-tab="' + tabName + '"]');
821
- if (btn) btn.classList.add('active');
822
- var content = document.getElementById('tab-' + tabName);
823
- if (content) content.classList.add('active');
824
- }
825
- document.querySelectorAll('.tab-btn').forEach(function(btn) {
826
- btn.addEventListener('click', function() { switchTab(btn.dataset.tab); });
827
- });
828
- document.querySelectorAll('.stat[data-nav]').forEach(function(stat) {
829
- stat.addEventListener('click', function() {
830
- switchTab(stat.dataset.nav);
831
- if (stat.dataset.section) {
832
- var el = document.getElementById(stat.dataset.section);
833
- if (el) setTimeout(function() { el.scrollIntoView({ behavior: 'smooth', block: 'start' }); }, 50);
834
- }
835
- });
836
- });
837
-
838
- var input = document.getElementById('search');
839
- var hint = document.querySelector('.search-hint');
840
-
841
- input.addEventListener('input', function(e) {
842
- var q = e.target.value.toLowerCase();
843
- hint.style.display = q ? 'none' : '';
844
- document.querySelectorAll('.repo-card').forEach(function(card) {
845
- var name = card.dataset.name.toLowerCase();
846
- var path = (card.dataset.path || '').toLowerCase();
847
- var text = card.textContent.toLowerCase();
848
- card.style.display = (q === '' || name.includes(q) || path.includes(q) || text.includes(q)) ? '' : 'none';
849
- });
850
- });
851
-
852
- document.addEventListener('keydown', function(e) {
853
- if (e.key === '/' && document.activeElement !== input) {
854
- e.preventDefault();
855
- // Switch to repos tab first
856
- document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
857
- document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
858
- document.querySelector('[data-tab="repos"]').classList.add('active');
859
- document.getElementById('tab-repos').classList.add('active');
860
- input.focus();
861
- }
862
- if (e.key === 'Escape' && document.activeElement === input) {
863
- input.value = '';
864
- input.dispatchEvent(new Event('input'));
865
- input.blur();
866
- }
867
- });
868
-
869
- var toggle = document.getElementById('theme-toggle');
870
- var saved = localStorage.getItem('ccd-theme');
871
- if (saved) document.documentElement.setAttribute('data-theme', saved);
872
- else if (window.matchMedia('(prefers-color-scheme: light)').matches) {
873
- document.documentElement.setAttribute('data-theme', 'light');
874
- }
875
- toggle.addEventListener('click', function() {
876
- var current = document.documentElement.getAttribute('data-theme');
877
- var next = current === 'light' ? 'dark' : 'light';
878
- document.documentElement.setAttribute('data-theme', next);
879
- localStorage.setItem('ccd-theme', next);
880
- });
881
-
882
- var groupSelect = document.getElementById('group-by');
883
- groupSelect.addEventListener('change', function() {
884
- var mode = this.value;
885
- var grid = document.getElementById('repo-grid');
886
- grid.querySelectorAll('.group-heading').forEach(function(h) { h.remove(); });
887
- var cards = Array.from(grid.querySelectorAll('.repo-card'));
888
- if (mode === 'none') {
889
- cards.forEach(function(c) { grid.appendChild(c); });
890
- return;
891
- }
892
- var groups = {};
893
- cards.forEach(function(card) {
894
- if (mode === 'stack') {
895
- var stacks = (card.dataset.stack || 'undetected').split(',');
896
- stacks.forEach(function(s) {
897
- var key = s.trim() || 'undetected';
898
- if (!groups[key]) groups[key] = [];
899
- groups[key].push(card);
900
- });
901
- } else {
902
- var key = card.dataset.parent || '~/';
903
- if (!groups[key]) groups[key] = [];
904
- groups[key].push(card);
905
- }
906
- });
907
- Object.keys(groups).sort().forEach(function(key) {
908
- var h = document.createElement('div');
909
- h.className = 'group-heading';
910
- h.textContent = key || '(none)';
911
- grid.appendChild(h);
912
- groups[key].forEach(function(card) { grid.appendChild(card); });
913
- });
914
- });
915
-
916
- // Custom tooltip for heatmap cells and peak bars
917
- var tip = document.getElementById('chart-tooltip');
918
- document.addEventListener('mouseover', function(e) {
919
- var t = e.target.closest('.heatmap-cell, .peak-bar');
920
- if (t && t.title) {
921
- tip.textContent = t.title;
922
- tip.classList.add('visible');
923
- t.dataset.tip = t.title;
924
- t.removeAttribute('title');
925
- }
926
- });
927
- document.addEventListener('mousemove', function(e) {
928
- if (tip.classList.contains('visible')) {
929
- tip.style.left = (e.clientX + 12) + 'px';
930
- tip.style.top = (e.clientY - 28) + 'px';
931
- }
932
- });
933
- document.addEventListener('mouseout', function(e) {
934
- var t = e.target.closest('.heatmap-cell, .peak-bar');
935
- if (t && t.dataset.tip) {
936
- t.title = t.dataset.tip;
937
- delete t.dataset.tip;
938
- }
939
- if (!e.relatedTarget || !e.relatedTarget.closest || !e.relatedTarget.closest('.heatmap-cell, .peak-bar')) {
940
- tip.classList.remove('visible');
941
- }
942
- });
943
- </script>
944
- </body>
945
- </html>`;
946
- }