@viren/claude-code-dashboard 0.0.3 → 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.
@@ -260,6 +260,7 @@ function parseChains() {
260
260
  const chains = parseChains();
261
261
 
262
262
  // MCP Server Discovery
263
+ const claudeJsonPath = join(HOME, ".claude.json");
263
264
  const allMcpServers = [];
264
265
 
265
266
  const userMcpPath = join(CLAUDE_DIR, "mcp_config.json");
@@ -272,6 +273,21 @@ if (existsSync(userMcpPath)) {
272
273
  }
273
274
  }
274
275
 
276
+ // ~/.claude.json is the primary location where `claude mcp add` writes
277
+ let claudeJsonParsed = null;
278
+ if (existsSync(claudeJsonPath)) {
279
+ try {
280
+ const content = readFileSync(claudeJsonPath, "utf8");
281
+ claudeJsonParsed = JSON.parse(content);
282
+ const existing = new Set(allMcpServers.filter((s) => s.scope === "user").map((s) => s.name));
283
+ for (const s of parseUserMcpConfig(content)) {
284
+ if (!existing.has(s.name)) allMcpServers.push(s);
285
+ }
286
+ } catch {
287
+ // skip if unreadable
288
+ }
289
+ }
290
+
275
291
  for (const repoDir of allRepoPaths) {
276
292
  const mcpPath = join(repoDir, ".mcp.json");
277
293
  if (existsSync(mcpPath)) {
@@ -290,11 +306,9 @@ for (const repoDir of allRepoPaths) {
290
306
 
291
307
  // Disabled MCP servers
292
308
  const disabledMcpByRepo = {};
293
- const claudeJsonPath = join(HOME, ".claude.json");
294
- if (existsSync(claudeJsonPath)) {
309
+ if (claudeJsonParsed) {
295
310
  try {
296
- const claudeJsonContent = readFileSync(claudeJsonPath, "utf8");
297
- const claudeJson = JSON.parse(claudeJsonContent);
311
+ const claudeJson = claudeJsonParsed;
298
312
  for (const [path, entry] of Object.entries(claudeJson)) {
299
313
  if (
300
314
  typeof entry === "object" &&
@@ -397,35 +411,98 @@ let ccusageData = null;
397
411
  const ccusageCachePath = join(CLAUDE_DIR, "ccusage-cache.json");
398
412
  const CCUSAGE_TTL_MS = 60 * 60 * 1000;
399
413
 
400
- if (!cliArgs.quiet) {
414
+ try {
415
+ const cached = JSON.parse(readFileSync(ccusageCachePath, "utf8"));
416
+ if (cached._ts && Date.now() - cached._ts < CCUSAGE_TTL_MS && cached.totals && cached.daily) {
417
+ ccusageData = cached;
418
+ }
419
+ } catch {
420
+ /* no cache or stale */
421
+ }
422
+
423
+ if (!ccusageData) {
401
424
  try {
402
- const cached = JSON.parse(readFileSync(ccusageCachePath, "utf8"));
403
- if (cached._ts && Date.now() - cached._ts < CCUSAGE_TTL_MS && cached.totals && cached.daily) {
404
- ccusageData = cached;
425
+ const raw = execFileSync("npx", ["ccusage", "--json"], {
426
+ encoding: "utf8",
427
+ timeout: 30_000,
428
+ stdio: ["pipe", "pipe", "pipe"],
429
+ });
430
+ const parsed = JSON.parse(raw);
431
+ if (parsed.totals && parsed.daily) {
432
+ ccusageData = parsed;
433
+ try {
434
+ writeFileSync(ccusageCachePath, JSON.stringify({ ...parsed, _ts: Date.now() }));
435
+ } catch {
436
+ /* non-critical */
437
+ }
405
438
  }
406
439
  } catch {
407
- /* no cache or stale */
440
+ // ccusage not installed or timed out
408
441
  }
442
+ }
409
443
 
410
- if (!ccusageData) {
411
- try {
412
- const raw = execFileSync("npx", ["ccusage", "--json"], {
413
- encoding: "utf8",
414
- timeout: 30_000,
415
- stdio: ["pipe", "pipe", "pipe"],
444
+ // Claude Code Insights report (generated by /insights)
445
+ let insightsReport = null;
446
+ const reportPath = join(CLAUDE_DIR, "usage-data", "report.html");
447
+ if (existsSync(reportPath)) {
448
+ try {
449
+ const reportHtml = readFileSync(reportPath, "utf8");
450
+
451
+ // Extract subtitle — reformat ISO dates to readable format
452
+ const subtitleMatch = reportHtml.match(/<p class="subtitle">([^<]+)<\/p>/);
453
+ let subtitle = subtitleMatch ? subtitleMatch[1] : null;
454
+ if (subtitle) {
455
+ subtitle = subtitle.replace(/(\d{4})-(\d{2})-(\d{2})/g, (_, y, m2, d) => {
456
+ const dt = new Date(`${y}-${m2}-${d}T00:00:00Z`);
457
+ return dt.toLocaleDateString("en-US", {
458
+ month: "short",
459
+ day: "numeric",
460
+ year: "numeric",
461
+ timeZone: "UTC",
462
+ });
416
463
  });
417
- const parsed = JSON.parse(raw);
418
- if (parsed.totals && parsed.daily) {
419
- ccusageData = parsed;
420
- try {
421
- writeFileSync(ccusageCachePath, JSON.stringify({ ...parsed, _ts: Date.now() }));
422
- } catch {
423
- /* non-critical */
424
- }
425
- }
426
- } catch {
427
- // ccusage not installed or timed out
428
464
  }
465
+
466
+ // Extract glance sections (content may contain <strong> tags)
467
+ const glanceSections = [];
468
+ const glanceRe =
469
+ /<div class="glance-section"><strong>([^<]+)<\/strong>\s*([\s\S]*?)<a[^>]*class="see-more"/g;
470
+ let m;
471
+ while ((m = glanceRe.exec(reportHtml)) !== null) {
472
+ const text = m[2].replace(/<[^>]+>/g, "").trim();
473
+ glanceSections.push({ label: m[1].replace(/:$/, ""), text });
474
+ }
475
+
476
+ // Extract stats
477
+ const statsRe = /<div class="stat-value">([^<]+)<\/div><div class="stat-label">([^<]+)<\/div>/g;
478
+ const reportStats = [];
479
+ while ((m = statsRe.exec(reportHtml)) !== null) {
480
+ const value = m[1];
481
+ const label = m[2];
482
+ // Mark lines stat for diff-style rendering
483
+ const isDiff = /^[+-]/.test(value) && value.includes("/");
484
+ reportStats.push({ value, label, isDiff });
485
+ }
486
+
487
+ // Extract friction categories
488
+ const frictionRe =
489
+ /<div class="friction-title">([^<]+)<\/div>\s*<div class="friction-desc">([^<]+)<\/div>/g;
490
+ const frictionPoints = [];
491
+ while ((m = frictionRe.exec(reportHtml)) !== null) {
492
+ frictionPoints.push({ title: m[1], desc: m[2] });
493
+ }
494
+
495
+ if (glanceSections.length > 0 || reportStats.length > 0) {
496
+ insightsReport = {
497
+ subtitle,
498
+ glance: glanceSections,
499
+ stats: reportStats,
500
+ friction: frictionPoints.slice(0, 3),
501
+ filePath: reportPath,
502
+ };
503
+ }
504
+ } catch {
505
+ // skip if unreadable
429
506
  }
430
507
  }
431
508
 
@@ -463,6 +540,19 @@ if (sessionMetaFiles.length > 0) {
463
540
  }
464
541
  }
465
542
 
543
+ // Supplement dailyActivity with ccusage data (fills gaps like Feb 17-22)
544
+ if (ccusageData && ccusageData.daily) {
545
+ const existingDates = new Set((statsCache.dailyActivity || []).map((d) => d.date));
546
+ const ccusageSupplemental = ccusageData.daily
547
+ .filter((d) => d.date && !existingDates.has(d.date) && d.totalTokens > 0)
548
+ .map((d) => ({ date: d.date, messageCount: Math.max(1, Math.round(d.totalTokens / 10000)) }));
549
+ if (ccusageSupplemental.length > 0) {
550
+ statsCache.dailyActivity = [...(statsCache.dailyActivity || []), ...ccusageSupplemental].sort(
551
+ (a, b) => a.date.localeCompare(b.date),
552
+ );
553
+ }
554
+ }
555
+
466
556
  // ── Computed Stats ───────────────────────────────────────────────────────────
467
557
 
468
558
  const totalRepos = allRepoPaths.length;
@@ -478,6 +568,107 @@ const driftCount = configured.filter(
478
568
  (r) => r.drift && (r.drift.level === "medium" || r.drift.level === "high"),
479
569
  ).length;
480
570
 
571
+ // ── Insights ──────────────────────────────────────────────────────────────────
572
+ const insights = [];
573
+
574
+ // Drift alerts
575
+ const highDriftRepos = configured.filter((r) => r.drift?.level === "high");
576
+ if (highDriftRepos.length > 0) {
577
+ insights.push({
578
+ type: "warning",
579
+ title: `${highDriftRepos.length} repo${highDriftRepos.length > 1 ? "s have" : " has"} high config drift`,
580
+ detail: highDriftRepos
581
+ .map((r) => `${r.name} (${r.drift.commitsSince} commits since config update)`)
582
+ .join(", "),
583
+ action: "Review and update CLAUDE.md in these repos",
584
+ });
585
+ }
586
+
587
+ // Coverage
588
+ if (unconfigured.length > 0 && totalRepos > 0) {
589
+ const pct = Math.round((unconfigured.length / totalRepos) * 100);
590
+ if (pct >= 40) {
591
+ const withStack = unconfigured.filter((r) => r.techStack?.length > 0).slice(0, 3);
592
+ insights.push({
593
+ type: "info",
594
+ title: `${unconfigured.length} repos unconfigured (${pct}%)`,
595
+ detail: withStack.length
596
+ ? `Top candidates: ${withStack.map((r) => `${r.name} (${r.techStack.join(", ")})`).join(", ")}`
597
+ : "",
598
+ action: "Run claude-code-dashboard init --template <stack> in these repos",
599
+ });
600
+ }
601
+ }
602
+
603
+ // MCP promotions
604
+ if (mcpPromotions.length > 0) {
605
+ insights.push({
606
+ type: "promote",
607
+ title: `${mcpPromotions.length} MCP server${mcpPromotions.length > 1 ? "s" : ""} could be promoted to global`,
608
+ detail: mcpPromotions.map((p) => `${p.name} (in ${p.projects.length} projects)`).join(", "),
609
+ action: "Add to ~/.claude/mcp_config.json for all projects",
610
+ });
611
+ }
612
+
613
+ // Redundant project-scope MCP configs (global server also in project .mcp.json)
614
+ const redundantMcp = Object.values(mcpByName).filter((s) => s.userLevel && s.projects.length > 0);
615
+ if (redundantMcp.length > 0) {
616
+ insights.push({
617
+ type: "tip",
618
+ title: `${redundantMcp.length} MCP server${redundantMcp.length > 1 ? "s are" : " is"} global but also in project .mcp.json`,
619
+ detail: redundantMcp.map((s) => `${s.name} (${s.projects.join(", ")})`).join("; "),
620
+ action: "Remove from project .mcp.json — global config already covers all projects",
621
+ });
622
+ }
623
+
624
+ // Skill sharing opportunities
625
+ const skillMatchCounts = {};
626
+ for (const r of configured) {
627
+ for (const sk of r.matchedSkills || []) {
628
+ const skName = typeof sk === "string" ? sk : sk.name;
629
+ if (!skillMatchCounts[skName]) skillMatchCounts[skName] = [];
630
+ skillMatchCounts[skName].push(r.name);
631
+ }
632
+ }
633
+ const widelyRelevant = Object.entries(skillMatchCounts)
634
+ .filter(([, repos]) => repos.length >= 3)
635
+ .sort((a, b) => b[1].length - a[1].length);
636
+ if (widelyRelevant.length > 0) {
637
+ const top = widelyRelevant.slice(0, 3);
638
+ insights.push({
639
+ type: "info",
640
+ title: `${widelyRelevant.length} skill${widelyRelevant.length > 1 ? "s" : ""} relevant across 3+ repos`,
641
+ detail: top.map(([name, repos]) => `${name} (${repos.length} repos)`).join(", "),
642
+ action: "Consider adding these skills to your global config",
643
+ });
644
+ }
645
+
646
+ // Health quick wins — repos closest to next tier
647
+ const quickWinRepos = configured
648
+ .filter((r) => r.healthScore > 0 && r.healthScore < 80 && r.healthReasons?.length > 0)
649
+ .sort((a, b) => b.healthScore - a.healthScore)
650
+ .slice(0, 3);
651
+ if (quickWinRepos.length > 0) {
652
+ insights.push({
653
+ type: "tip",
654
+ title: "Quick wins to improve config health",
655
+ detail: quickWinRepos
656
+ .map((r) => `${r.name} (${r.healthScore}/100): ${r.healthReasons[0]}`)
657
+ .join("; "),
658
+ action: "Small changes for measurable improvement",
659
+ });
660
+ }
661
+
662
+ // Insights report nudge
663
+ if (!insightsReport) {
664
+ insights.push({
665
+ type: "info",
666
+ title: "Generate your Claude Code Insights report",
667
+ detail: "Get personalized usage patterns, friction points, and feature suggestions",
668
+ action: "Run /insights in Claude Code",
669
+ });
670
+ }
671
+
481
672
  const now = new Date();
482
673
  const timestamp =
483
674
  now
@@ -684,6 +875,8 @@ const html = generateDashboardHtml({
684
875
  driftCount,
685
876
  mcpCount,
686
877
  scanScope,
878
+ insights,
879
+ insightsReport,
687
880
  });
688
881
 
689
882
  // ── Write HTML Output ────────────────────────────────────────────────────────
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@viren/claude-code-dashboard",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "A visual dashboard for your Claude Code configuration across all repos",
5
5
  "type": "module",
6
6
  "bin": {
package/src/constants.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { join } from "path";
2
2
  import { homedir } from "os";
3
3
 
4
- export const VERSION = "0.0.3";
4
+ export const VERSION = "0.0.4";
5
5
  export const REPO_URL = "https://github.com/VirenMohindra/claude-code-dashboard";
6
6
 
7
7
  export const HOME = homedir();
package/src/demo.mjs CHANGED
@@ -484,7 +484,70 @@ export function generateDemoData() {
484
484
  ),
485
485
  driftCount: configured.filter((r) => r.drift.level === "medium" || r.drift.level === "high")
486
486
  .length,
487
- mcpCount: 4,
487
+ mcpCount: 5,
488
488
  scanScope: "~/work (depth 5)",
489
+ insights: [
490
+ {
491
+ type: "warning",
492
+ title: "2 repos have high config drift",
493
+ detail:
494
+ "payments-api (23 commits since config update), acme-web (18 commits since config update)",
495
+ action: "Review and update CLAUDE.md in these repos",
496
+ },
497
+ {
498
+ type: "promote",
499
+ title: "1 MCP server could be promoted to global",
500
+ detail: "github (in 2 projects)",
501
+ action: "Add to ~/.claude/mcp_config.json for all projects",
502
+ },
503
+ {
504
+ type: "info",
505
+ title: "12 repos unconfigured (52%)",
506
+ detail: "Top candidates: mobile-app (expo), admin-portal (next), data-pipeline (python)",
507
+ action: "Run claude-code-dashboard init --template <stack> in these repos",
508
+ },
509
+ {
510
+ type: "tip",
511
+ title: "Quick wins to improve config health",
512
+ detail:
513
+ "design-system (65/100): add commands; shared-utils (60/100): add CLAUDE.md description",
514
+ action: "Small changes for measurable improvement",
515
+ },
516
+ ],
517
+ insightsReport: {
518
+ subtitle: "1,386 messages across 117 sessions (365 total) | 2026-02-23 to 2026-03-10",
519
+ stats: [
520
+ { value: "1,386", label: "Messages" },
521
+ { value: "+33,424/-2,563", label: "Lines" },
522
+ { value: "632", label: "Files" },
523
+ { value: "14", label: "Days" },
524
+ { value: "99", label: "Msgs/Day" },
525
+ ],
526
+ glance: [
527
+ {
528
+ label: "What's working",
529
+ text: "Full end-to-end shipping workflow — implementation through PR creation to production deployment in single sessions.",
530
+ },
531
+ {
532
+ label: "What's hindering you",
533
+ text: "Claude frequently jumps into fixes without checking actual state first, costing correction cycles.",
534
+ },
535
+ {
536
+ label: "Quick wins to try",
537
+ text: "Create custom slash commands for repeated workflows like PR reviews and Slack message drafting.",
538
+ },
539
+ ],
540
+ friction: [
541
+ {
542
+ title: "Wrong Target / Misidentification",
543
+ desc: "Claude acts on the wrong file or setting before you catch the mistake.",
544
+ },
545
+ {
546
+ title: "Premature Solutions",
547
+ desc: "Jumps into fixes without first checking actual state of the codebase.",
548
+ },
549
+ ],
550
+ filePath: "~/.claude/usage-data/report.html",
551
+ },
489
552
  };
490
553
  }
@@ -33,6 +33,8 @@ export function generateDashboardHtml({
33
33
  driftCount,
34
34
  mcpCount,
35
35
  scanScope,
36
+ insights,
37
+ insightsReport,
36
38
  }) {
37
39
  // ── Build tab content sections ──────────────────────────────────────────
38
40
 
@@ -49,7 +51,7 @@ export function generateDashboardHtml({
49
51
  `</details>`,
50
52
  )
51
53
  .join("\n ");
52
- return `<div class="card">
54
+ return `<div class="card" id="section-skills">
53
55
  <h2>Skills <span class="n">${globalSkills.length}</span></h2>
54
56
  ${categoryHtml}
55
57
  </div>`;
@@ -72,9 +74,10 @@ export function generateDashboardHtml({
72
74
  ? `<span class="badge mcp-recent">recent</span>`
73
75
  : `<span class="badge mcp-project">project</span>`;
74
76
  const typeBadge = `<span class="badge mcp-type">${esc(s.type)}</span>`;
75
- const projects = s.projects.length
76
- ? `<span class="mcp-projects">${s.projects.map((p) => esc(p)).join(", ")}</span>`
77
- : "";
77
+ const projects =
78
+ !s.userLevel && s.projects.length
79
+ ? `<span class="mcp-projects">${s.projects.map((p) => esc(p)).join(", ")}</span>`
80
+ : "";
78
81
  return `<div class="mcp-row${disabledClass}"><span class="mcp-name">${esc(s.name)}</span>${scopeBadge}${typeBadge}${disabledHint}${projects}</div>`;
79
82
  })
80
83
  .join("\n ");
@@ -97,7 +100,7 @@ export function generateDashboardHtml({
97
100
  })
98
101
  .join("\n ")}`
99
102
  : "";
100
- return `<div class="card">
103
+ return `<div class="card" id="section-mcp">
101
104
  <h2>MCP Servers <span class="n">${mcpSummary.length}</span></h2>
102
105
  ${rows}
103
106
  ${promoteHtml}
@@ -225,14 +228,21 @@ export function generateDashboardHtml({
225
228
  while (cursor2 <= lastDate) {
226
229
  const key = cursor2.toISOString().slice(0, 10);
227
230
  const count = dateMap.get(key) || 0;
228
- cells += `<div class="heatmap-cell${level(count)}" title="${esc(key)}: ${count} messages"></div>`;
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>`;
229
237
  cursor2.setUTCDate(cursor2.getUTCDate() + 1);
230
238
  }
231
239
 
232
240
  content += `<div class="label">Activity</div>
233
- <div style="position:relative;margin-bottom:.5rem">
234
- <div class="heatmap-months" style="position:relative;height:.8rem">${monthLabels}</div>
235
- <div style="overflow-x:auto"><div class="heatmap">${cells}</div></div>
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>
236
246
  </div>`;
237
247
  }
238
248
 
@@ -300,7 +310,7 @@ export function generateDashboardHtml({
300
310
  ${modelRows}`;
301
311
  }
302
312
 
303
- return `<div class="card">
313
+ return `<div class="card" id="section-activity">
304
314
  <h2>Activity</h2>
305
315
  ${content}
306
316
  </div>`;
@@ -452,7 +462,8 @@ export function generateDashboardHtml({
452
462
  .chain-arrow { color: var(--text-dim); font-size: .85rem; }
453
463
 
454
464
  .stats { display: grid; grid-template-columns: repeat(auto-fit, minmax(110px, 1fr)); gap: .65rem; margin-bottom: 1.5rem; }
455
- .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); }
456
467
  .stat b { display: block; font-size: 1.4rem; color: var(--accent); }
457
468
  .stat span { font-size: .6rem; color: var(--text-dim); text-transform: uppercase; letter-spacing: .06em; }
458
469
  .stat.coverage b { color: ${coveragePct >= 70 ? "var(--green)" : coveragePct >= 40 ? "var(--yellow)" : "var(--red)"}; }
@@ -524,6 +535,31 @@ export function generateDashboardHtml({
524
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; }
525
536
  .mcp-promote .mcp-name { color: var(--yellow); }
526
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; }
527
563
  .mcp-former { opacity: .4; }
528
564
  .badge.mcp-former-badge { color: var(--text-dim); border-color: var(--border); background: var(--surface2); font-style: italic; }
529
565
 
@@ -550,6 +586,9 @@ export function generateDashboardHtml({
550
586
  .heatmap-months { display: flex; font-size: .5rem; color: var(--text-dim); margin-bottom: .2rem; }
551
587
  .heatmap-month { flex: 1; }
552
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
+
553
592
  .peak-hours { display: flex; align-items: flex-end; gap: 2px; height: 40px; }
554
593
  .peak-bar { flex: 1; background: var(--purple); border-radius: 2px 2px 0 0; min-width: 4px; opacity: .7; }
555
594
  .peak-labels { display: flex; gap: 2px; font-size: .45rem; color: var(--text-dim); }
@@ -647,16 +686,16 @@ export function generateDashboardHtml({
647
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>
648
687
 
649
688
  <div class="stats">
650
- <div class="stat coverage"><b>${coveragePct}%</b><span>Coverage (${configuredCount}/${totalRepos})</span></div>
651
- <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>
652
- <div class="stat"><b>${globalCmds.length}</b><span>Global Commands</span></div>
653
- <div class="stat"><b>${globalSkills.length}</b><span>Skills</span></div>
654
- <div class="stat"><b>${totalRepoCmds}</b><span>Repo Commands</span></div>
655
- ${mcpCount > 0 ? `<div class="stat"><b>${mcpCount}</b><span>MCP Servers</span></div>` : ""}
656
- ${driftCount > 0 ? `<div class="stat" style="border-color:#f8717133"><b style="color:var(--red)">${driftCount}</b><span>Drifting Repos</span></div>` : ""}
657
- ${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>` : ""}
658
- ${ccusageData ? `<div class="stat"><b>${formatTokens(ccusageData.totals.totalTokens).replace(" tokens", "")}</b><span>Total Tokens</span></div>` : ""}
659
- ${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>` : ""}
660
699
  </div>
661
700
 
662
701
  <nav class="tab-nav">
@@ -669,7 +708,7 @@ export function generateDashboardHtml({
669
708
 
670
709
  <div class="tab-content active" id="tab-overview">
671
710
  <div class="top-grid">
672
- <div class="card" style="margin-bottom:0">
711
+ <div class="card" id="section-commands" style="margin-bottom:0">
673
712
  <h2>Global Commands <span class="n">${globalCmds.length}</span></h2>
674
713
  ${globalCmds.map((c) => renderCmd(c)).join("\n ")}
675
714
  </div>
@@ -678,6 +717,26 @@ export function generateDashboardHtml({
678
717
  ${globalRules.map((r) => renderRule(r)).join("\n ")}
679
718
  </div>
680
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
+ }
681
740
  ${chainsHtml}
682
741
  ${consolidationHtml}
683
742
  </div>
@@ -688,6 +747,38 @@ export function generateDashboardHtml({
688
747
  </div>
689
748
 
690
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
+ }
691
782
  <div class="top-grid">
692
783
  ${toolsHtml || ""}
693
784
  ${langsHtml || ""}
@@ -721,13 +812,26 @@ export function generateDashboardHtml({
721
812
 
722
813
  <div class="ts">found ${totalRepos} repos · ${configuredCount} configured · ${unconfiguredCount} unconfigured · scanned ${scanScope} · ${timestamp}</div>
723
814
 
815
+ <div class="chart-tooltip" id="chart-tooltip"></div>
724
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
+ }
725
825
  document.querySelectorAll('.tab-btn').forEach(function(btn) {
726
- btn.addEventListener('click', function() {
727
- document.querySelectorAll('.tab-btn').forEach(function(b) { b.classList.remove('active'); });
728
- document.querySelectorAll('.tab-content').forEach(function(c) { c.classList.remove('active'); });
729
- btn.classList.add('active');
730
- document.getElementById('tab-' + btn.dataset.tab).classList.add('active');
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
+ }
731
835
  });
732
836
  });
733
837
 
@@ -808,6 +912,34 @@ groupSelect.addEventListener('change', function() {
808
912
  groups[key].forEach(function(card) { grid.appendChild(card); });
809
913
  });
810
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
+ });
811
943
  </script>
812
944
  </body>
813
945
  </html>`;
package/src/watch.mjs CHANGED
@@ -13,8 +13,8 @@ export function startWatch(outputPath, scanRoots, cliArgs) {
13
13
  // and noisy snapshot writes on every file change
14
14
  const forwardedArgs = process.argv
15
15
  .slice(2)
16
- .filter((a) => a !== "--watch" && a !== "--diff")
17
- .concat(["--quiet"]);
16
+ .filter((a) => a !== "--watch" && a !== "--diff" && a !== "--open")
17
+ .concat(["--quiet", "--no-open"]);
18
18
 
19
19
  // Resolve output path to detect and ignore self-writes
20
20
  const resolvedOutput = resolve(outputPath);