@viren/claude-code-dashboard 0.0.2 → 0.0.4

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,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,
@@ -33,7 +33,346 @@ export function generateDashboardHtml({
33
33
  driftCount,
34
34
  mcpCount,
35
35
  scanScope,
36
+ insights,
37
+ insightsReport,
36
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
+
37
376
  return `<!DOCTYPE html>
38
377
  <html lang="en">
39
378
  <head>
@@ -61,14 +400,28 @@ export function generateDashboardHtml({
61
400
  }
62
401
  code, .cmd-name { font-family: 'SF Mono', 'Fira Code', 'JetBrains Mono', monospace; }
63
402
  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: 1.5rem; }
403
+ .sub { color: var(--text-dim); font-size: .78rem; margin-bottom: 1rem; }
65
404
  kbd { background: var(--surface2); border: 1px solid var(--border); border-radius: 3px; padding: .05rem .3rem; font-size: .7rem; font-family: inherit; }
66
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 ────────────────────────────────────────────────── */
67
419
  .top-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 1.25rem; margin-bottom: 1.25rem; }
420
+ .top-grid > .card { margin-bottom: 0; }
68
421
  @media (max-width: 900px) { .top-grid { grid-template-columns: 1fr; } }
69
422
 
70
- .card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 1.25rem; overflow: hidden; }
71
- .card.full { grid-column: 1 / -1; }
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; }
72
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; }
73
426
  .card h2 .n { background: var(--surface2); border: 1px solid var(--border); border-radius: 4px; padding: .05rem .35rem; font-size: .65rem; color: var(--accent); }
74
427
 
@@ -109,7 +462,8 @@ export function generateDashboardHtml({
109
462
  .chain-arrow { color: var(--text-dim); font-size: .85rem; }
110
463
 
111
464
  .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .65rem; margin-bottom: 1.5rem; }
112
- .stat { text-align: center; padding: .65rem .5rem; background: var(--surface); border: 1px solid var(--border); border-radius: 8px; }
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); }
113
467
  .stat b { display: block; font-size: 1.4rem; color: var(--accent); }
114
468
  .stat span { font-size: .6rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .06em; }
115
469
  .stat.coverage b { color: ${coveragePct >= 70 ? "var(--green)" : coveragePct >= 40 ? "var(--yellow)" : "var(--red)"}; }
@@ -136,6 +490,7 @@ export function generateDashboardHtml({
136
490
  .repo-card > summary {
137
491
  cursor: pointer; list-style: none; padding: .85rem 1rem;
138
492
  display: flex; flex-direction: column; gap: .3rem;
493
+ min-height: 7.5rem; justify-content: center;
139
494
  }
140
495
  .repo-card > summary::-webkit-details-marker { display: none; }
141
496
  .repo-card > summary:hover { background: var(--surface2); }
@@ -175,10 +530,36 @@ export function generateDashboardHtml({
175
530
  .mcp-projects { font-size: .65rem; color: var(--text-dim); margin-left: auto; }
176
531
  .badge.mcp-global { color: var(--green); border-color: #4ade8033; background: #4ade8010; }
177
532
  .badge.mcp-project { color: var(--blue); border-color: #60a5fa33; background: #60a5fa10; }
533
+ .badge.mcp-recent { color: var(--yellow); border-color: #fbbf2433; background: #fbbf2410; }
178
534
  .badge.mcp-type { color: var(--text-dim); border-color: var(--border); background: var(--surface2); text-transform: none; font-size: .5rem; }
179
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; }
180
536
  .mcp-promote .mcp-name { color: var(--yellow); }
181
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; }
182
563
  .mcp-former { opacity: .4; }
183
564
  .badge.mcp-former-badge { color: var(--text-dim); border-color: var(--border); background: var(--surface2); font-style: italic; }
184
565
 
@@ -191,8 +572,8 @@ export function generateDashboardHtml({
191
572
  .usage-bar-error { background: linear-gradient(90deg, var(--red), var(--yellow)); }
192
573
  .usage-bar-count { font-size: .65rem; color: var(--text-dim); min-width: 40px; text-align: right; font-variant-numeric: tabular-nums; }
193
574
 
194
- .heatmap { display: grid; grid-template-rows: repeat(7, 1fr); grid-auto-flow: column; grid-auto-columns: 1fr; gap: 2px; }
195
- .heatmap-cell { aspect-ratio: 1; border-radius: 2px; background: var(--surface2); min-width: 6px; min-height: 6px; }
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; }
196
577
  .heatmap-cell.l1 { background: #0e4429; }
197
578
  .heatmap-cell.l2 { background: #006d32; }
198
579
  .heatmap-cell.l3 { background: #26a641; }
@@ -205,6 +586,9 @@ export function generateDashboardHtml({
205
586
  .heatmap-months { display: flex; font-size: .5rem; color: var(--text-dim); margin-bottom: .2rem; }
206
587
  .heatmap-month { flex: 1; }
207
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
+
208
592
  .peak-hours { display: flex; align-items: flex-end; gap: 2px; height: 40px; }
209
593
  .peak-bar { flex: 1; background: var(--purple); border-radius: 2px 2px 0 0; min-width: 4px; opacity: .7; }
210
594
  .peak-labels { display: flex; gap: 2px; font-size: .45rem; color: var(--text-dim); }
@@ -231,6 +615,9 @@ export function generateDashboardHtml({
231
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); }
232
616
  .matched-skills { display: flex; flex-wrap: wrap; gap: .3rem; margin-bottom: .5rem; }
233
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; }
234
621
  .consolidation-hint { padding: .45rem .6rem; background: var(--surface2); border-radius: 6px; margin-top: .4rem; display: flex; align-items: baseline; gap: .5rem; }
235
622
  .consolidation-hint:first-child { margin-top: 0; }
236
623
  .consolidation-stack { font-size: .7rem; font-weight: 600; color: var(--accent); white-space: nowrap; }
@@ -296,386 +683,168 @@ export function generateDashboardHtml({
296
683
  <body>
297
684
  <h1>claude code dashboard</h1>
298
685
  <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 · click to expand</p>
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>
300
687
 
301
688
  <div class="stats">
302
- <div class="stat coverage"><b>${coveragePct}%</b><span>Coverage (${configuredCount}/${totalRepos})</span></div>
303
- <div class="stat" 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>
304
- <div class="stat"><b>${globalCmds.length}</b><span>Global Commands</span></div>
305
- <div class="stat"><b>${globalSkills.length}</b><span>Skills</span></div>
306
- <div class="stat"><b>${totalRepoCmds}</b><span>Repo Commands</span></div>
307
- ${mcpCount > 0 ? `<div class="stat"><b>${mcpCount}</b><span>MCP Servers</span></div>` : ""}
308
- ${driftCount > 0 ? `<div class="stat" style="border-color:#f8717133"><b style="color:var(--red)">${driftCount}</b><span>Drifting Repos</span></div>` : ""}
309
- ${ccusageData ? `<div class="stat" style="border-color:#4ade8033"><b style="color:var(--green)">$${Math.round(Number(ccusageData.totals.totalCost) || 0).toLocaleString()}</b><span>Total Spent</span></div>` : ""}
310
- ${ccusageData ? `<div class="stat"><b>${formatTokens(ccusageData.totals.totalTokens).replace(" tokens", "")}</b><span>Total Tokens</span></div>` : ""}
311
- ${usageAnalytics.heavySessions > 0 ? `<div class="stat"><b>${usageAnalytics.heavySessions}</b><span>Heavy Sessions</span></div>` : ""}
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>` : ""}
312
699
  </div>
313
700
 
314
- <div class="top-grid">
315
- <div class="card">
316
- <h2>Global Commands <span class="n">${globalCmds.length}</span></h2>
317
- ${globalCmds.map((c) => renderCmd(c)).join("\n ")}
318
- </div>
319
- <div class="card">
320
- <h2>Global Rules <span class="n">${globalRules.length}</span></h2>
321
- ${globalRules.map((r) => renderRule(r)).join("\n ")}
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}
322
742
  </div>
323
- ${
324
- globalSkills.length
325
- ? (() => {
326
- const groups = groupSkillsByCategory(globalSkills);
327
- const categoryHtml = Object.entries(groups)
328
- .map(
329
- ([cat, skills], idx) =>
330
- `<details class="skill-category"${idx === 0 ? " open" : ""}>` +
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 &rarr; 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
743
 
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
- }
744
+ <div class="tab-content" id="tab-skills-mcp">
745
+ ${skillsHtml}
746
+ ${mcpHtml}
747
+ </div>
516
748
 
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>`;
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
+ : ""
527
769
  }
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
- }
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
+ : ""
546
774
  }
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}`;
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>`
581
781
  }
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>
602
- </div>
782
+ <div class="top-grid">
783
+ ${toolsHtml || ""}
784
+ ${langsHtml || ""}
603
785
  </div>
604
- </details>
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
- }
786
+ ${errorsHtml}
787
+ ${activityHtml}
613
788
  </div>
614
789
 
615
- ${
616
- consolidationGroups.length
617
- ? `<div class="card full" style="margin-bottom:1.25rem">
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>
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}
635
807
  </div>
636
808
 
637
- <div class="repo-grid" id="repo-grid">
638
- ${configured.map((r) => renderRepoCard(r)).join("\n")}
809
+ <div class="tab-content" id="tab-reference">
810
+ ${referenceHtml}
639
811
  </div>
640
812
 
641
- ${
642
- unconfigured.length
643
- ? `<details class="card" style="margin-bottom:1.25rem">
644
- <summary style="cursor:pointer;list-style:none"><h2 style="margin:0">Unconfigured Repos <span class="n">${unconfiguredCount}</span></h2></summary>
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>
661
- </div>
662
- </details>`
663
- : ""
664
- }
665
-
666
813
  <div class="ts">found ${totalRepos} repos · ${configuredCount} configured · ${unconfiguredCount} unconfigured · scanned ${scanScope} · ${timestamp}</div>
667
814
 
815
+ <div class="chart-tooltip" id="chart-tooltip"></div>
668
816
  <script>
669
- const input = document.getElementById('search');
670
- const hint = document.querySelector('.search-hint');
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');
671
840
 
672
841
  input.addEventListener('input', function(e) {
673
- const q = e.target.value.toLowerCase();
842
+ var q = e.target.value.toLowerCase();
674
843
  hint.style.display = q ? 'none' : '';
675
844
  document.querySelectorAll('.repo-card').forEach(function(card) {
676
- const name = card.dataset.name.toLowerCase();
677
- const path = (card.dataset.path || '').toLowerCase();
678
- const text = card.textContent.toLowerCase();
845
+ var name = card.dataset.name.toLowerCase();
846
+ var path = (card.dataset.path || '').toLowerCase();
847
+ var text = card.textContent.toLowerCase();
679
848
  card.style.display = (q === '' || name.includes(q) || path.includes(q) || text.includes(q)) ? '' : 'none';
680
849
  });
681
850
  });
@@ -683,6 +852,11 @@ input.addEventListener('input', function(e) {
683
852
  document.addEventListener('keydown', function(e) {
684
853
  if (e.key === '/' && document.activeElement !== input) {
685
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');
686
860
  input.focus();
687
861
  }
688
862
  if (e.key === 'Escape' && document.activeElement === input) {
@@ -738,6 +912,34 @@ groupSelect.addEventListener('change', function() {
738
912
  groups[key].forEach(function(card) { grid.appendChild(card); });
739
913
  });
740
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
+ });
741
943
  </script>
742
944
  </body>
743
945
  </html>`;