@viren/claude-code-dashboard 0.0.3 → 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.
package/README.md CHANGED
@@ -31,9 +31,9 @@ Scans your home directory for git repos, collects Claude Code configuration (com
31
31
 
32
32
  ![Repo Expanded](screenshots/05-repo-expanded.png)
33
33
 
34
- ### Dark mode
34
+ ### Light mode
35
35
 
36
- ![Dark Mode](screenshots/06-dark-mode.png)
36
+ ![Light Mode](screenshots/06-light-mode.png)
37
37
 
38
38
  > Screenshots generated with `claude-code-dashboard --demo`
39
39
 
@@ -60,7 +60,7 @@ import {
60
60
  import { aggregateSessionMeta } from "./src/usage.mjs";
61
61
  import { handleInit } from "./src/templates.mjs";
62
62
  import { generateCatalogHtml } from "./src/render.mjs";
63
- import { generateDashboardHtml } from "./src/html-template.mjs";
63
+ import { generateDashboardHtml } from "./src/assembler.mjs";
64
64
  import { startWatch } from "./src/watch.mjs";
65
65
 
66
66
  // ── CLI ──────────────────────────────────────────────────────────────────────
@@ -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.5",
4
4
  "description": "A visual dashboard for your Claude Code configuration across all repos",
5
5
  "type": "module",
6
6
  "bin": {
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "scripts": {
10
10
  "generate": "node generate-dashboard.mjs",
11
- "test": "node --test test/helpers.test.mjs",
11
+ "test": "node --test test/*.test.mjs",
12
12
  "lint": "eslint .",
13
13
  "lint:fix": "eslint . --fix",
14
14
  "format": "prettier --write .",
@@ -33,7 +33,8 @@
33
33
  },
34
34
  "files": [
35
35
  "generate-dashboard.mjs",
36
- "src/"
36
+ "src/",
37
+ "template/"
37
38
  ],
38
39
  "devDependencies": {
39
40
  "eslint": "^9.0.0",
@@ -0,0 +1,150 @@
1
+ import { readFileSync } from "fs";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+
5
+ import { esc } from "./helpers.mjs";
6
+ import { VERSION, REPO_URL } from "./constants.mjs";
7
+ import { renderCmd, renderRule, renderRepoCard } from "./render.mjs";
8
+ import {
9
+ renderSkillsCard,
10
+ renderMcpCard,
11
+ renderToolsCard,
12
+ renderLangsCard,
13
+ renderErrorsCard,
14
+ renderActivityCard,
15
+ renderChainsCard,
16
+ renderConsolidationCard,
17
+ renderUnconfiguredCard,
18
+ renderReferenceCard,
19
+ renderInsightsCard,
20
+ renderInsightsReportCard,
21
+ renderStatsBar,
22
+ } from "./sections.mjs";
23
+
24
+ // Resolve template directory relative to this module (works when installed via npm too)
25
+ const __dirname = dirname(fileURLToPath(import.meta.url));
26
+ const TEMPLATE_DIR = join(__dirname, "..", "template");
27
+
28
+ // Cache template files (read once per process).
29
+ // Assumes one-shot CLI usage; watch mode spawns fresh processes.
30
+ let _css, _js, _html;
31
+ function loadTemplates() {
32
+ if (!_css) _css = readFileSync(join(TEMPLATE_DIR, "dashboard.css"), "utf8");
33
+ if (!_js) _js = readFileSync(join(TEMPLATE_DIR, "dashboard.js"), "utf8");
34
+ if (!_html) _html = readFileSync(join(TEMPLATE_DIR, "dashboard.html"), "utf8");
35
+ }
36
+
37
+ export function generateDashboardHtml(data) {
38
+ loadTemplates();
39
+
40
+ const {
41
+ configured,
42
+ unconfigured,
43
+ globalCmds,
44
+ globalRules,
45
+ globalSkills,
46
+ chains,
47
+ mcpSummary,
48
+ mcpPromotions,
49
+ formerMcpServers,
50
+ consolidationGroups,
51
+ usageAnalytics,
52
+ ccusageData,
53
+ statsCache,
54
+ timestamp,
55
+ coveragePct,
56
+ totalRepos,
57
+ configuredCount,
58
+ unconfiguredCount,
59
+ scanScope,
60
+ insights,
61
+ insightsReport,
62
+ } = data;
63
+
64
+ // ── Build section HTML fragments ──────────────────────────────────────────
65
+
66
+ const header = `<h1>claude code dashboard</h1>
67
+ <button id="theme-toggle" class="theme-toggle" title="Toggle light/dark mode" aria-label="Toggle theme"><span class="theme-icon"></span></button>
68
+ <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>`;
69
+
70
+ const statsBar = renderStatsBar(data);
71
+
72
+ // Overview tab
73
+ const overviewCommands = `<div class="top-grid">
74
+ <div class="card" id="section-commands" style="margin-bottom:0">
75
+ <h2>Global Commands <span class="n">${globalCmds.length}</span></h2>
76
+ ${globalCmds.map((c) => renderCmd(c)).join("\n ")}
77
+ </div>
78
+ <div class="card" style="margin-bottom:0">
79
+ <h2>Global Rules <span class="n">${globalRules.length}</span></h2>
80
+ ${globalRules.map((r) => renderRule(r)).join("\n ")}
81
+ </div>
82
+ </div>`;
83
+ const insightsHtml = renderInsightsCard(insights);
84
+ const chainsHtml = renderChainsCard(chains);
85
+ const consolidationHtml = renderConsolidationCard(consolidationGroups);
86
+ const tabOverview = `${overviewCommands}\n ${insightsHtml}\n ${chainsHtml}\n ${consolidationHtml}`;
87
+
88
+ // Skills & MCP tab
89
+ const tabSkillsMcp = `${renderSkillsCard(globalSkills)}\n ${renderMcpCard(mcpSummary, mcpPromotions, formerMcpServers)}`;
90
+
91
+ // Analytics tab
92
+ const insightsReportHtml = renderInsightsReportCard(insightsReport);
93
+ const toolsHtml = renderToolsCard(usageAnalytics.topTools);
94
+ const langsHtml = renderLangsCard(usageAnalytics.topLanguages);
95
+ const errorsHtml = renderErrorsCard(usageAnalytics.errorCategories);
96
+ const activityHtml = renderActivityCard(statsCache, ccusageData);
97
+ const tabAnalytics = `${insightsReportHtml}
98
+ <div class="top-grid">
99
+ ${toolsHtml || ""}
100
+ ${langsHtml || ""}
101
+ </div>
102
+ ${errorsHtml}
103
+ ${activityHtml}`;
104
+
105
+ // Repos tab
106
+ const repoCards = configured.map((r) => renderRepoCard(r)).join("\n");
107
+ const unconfiguredHtml = renderUnconfiguredCard(unconfigured);
108
+ const tabRepos = `<div class="search-bar">
109
+ <input type="text" id="search" placeholder="search repos..." autocomplete="off">
110
+ <span class="search-hint"><kbd>/</kbd></span>
111
+ </div>
112
+ <div class="group-controls">
113
+ <label class="group-label">Group by:</label>
114
+ <select id="group-by" class="group-select">
115
+ <option value="none">None</option>
116
+ <option value="stack">Tech Stack</option>
117
+ <option value="parent">Parent Directory</option>
118
+ </select>
119
+ </div>
120
+ <div class="repo-grid" id="repo-grid">
121
+ ${repoCards}
122
+ </div>
123
+ ${unconfiguredHtml}`;
124
+
125
+ // Reference tab
126
+ const tabReference = renderReferenceCard();
127
+
128
+ // Footer
129
+ const footer = `<div class="ts">found ${totalRepos} repos · ${configuredCount} configured · ${unconfiguredCount} unconfigured · scanned ${scanScope} · ${timestamp}</div>`;
130
+
131
+ // ── Inject dynamic coverage color via CSS custom property ─────────────────
132
+ const coverageColor =
133
+ coveragePct >= 70 ? "var(--green)" : coveragePct >= 40 ? "var(--yellow)" : "var(--red)";
134
+ const css = `:root { --coverage-color: ${coverageColor}; }\n${_css}`;
135
+
136
+ // ── Assemble final HTML via placeholder replacement ───────────────────────
137
+ let html = _html;
138
+ html = html.replace("<!-- {{CSS}} -->", css);
139
+ html = html.replace("/* {{JS}} */", _js);
140
+ html = html.replace("<!-- {{HEADER}} -->", header);
141
+ html = html.replace("<!-- {{STATS_BAR}} -->", statsBar);
142
+ html = html.replace("<!-- {{TAB_OVERVIEW}} -->", tabOverview);
143
+ html = html.replace("<!-- {{TAB_SKILLS_MCP}} -->", tabSkillsMcp);
144
+ html = html.replace("<!-- {{TAB_ANALYTICS}} -->", tabAnalytics);
145
+ html = html.replace("<!-- {{TAB_REPOS}} -->", tabRepos);
146
+ html = html.replace("<!-- {{TAB_REFERENCE}} -->", tabReference);
147
+ html = html.replace("<!-- {{FOOTER}} -->", footer);
148
+
149
+ return html;
150
+ }
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.5";
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
  }
package/src/render.mjs CHANGED
@@ -1,6 +1,5 @@
1
1
  import { esc } from "./helpers.mjs";
2
2
  import { extractSteps, extractSections } from "./markdown.mjs";
3
- import { groupSkillsByCategory } from "./skills.mjs";
4
3
 
5
4
  export function renderSections(sections) {
6
5
  return sections
@@ -58,9 +57,6 @@ export function renderSkill(skill) {
58
57
  return `<div class="cmd-row"><span class="cmd-name skill-name">${esc(skill.name)}</span>${badge}<span class="cmd-desc">${d}</span></div>`;
59
58
  }
60
59
 
61
- // Re-export from skills.mjs (single source of truth)
62
- export { groupSkillsByCategory };
63
-
64
60
  export function renderBadges(repo) {
65
61
  const b = [];
66
62
  if (repo.commands.length) b.push(`<span class="badge cmds">${repo.commands.length} cmd</span>`);