@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.
- package/generate-dashboard.mjs +219 -26
- package/package.json +1 -1
- package/src/constants.mjs +1 -1
- package/src/demo.mjs +64 -1
- package/src/html-template.mjs +159 -27
- package/src/watch.mjs +2 -2
package/generate-dashboard.mjs
CHANGED
|
@@ -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
|
-
|
|
294
|
-
if (existsSync(claudeJsonPath)) {
|
|
309
|
+
if (claudeJsonParsed) {
|
|
295
310
|
try {
|
|
296
|
-
const
|
|
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
|
-
|
|
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
|
|
403
|
-
|
|
404
|
-
|
|
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
|
-
|
|
440
|
+
// ccusage not installed or timed out
|
|
408
441
|
}
|
|
442
|
+
}
|
|
409
443
|
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
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
package/src/constants.mjs
CHANGED
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:
|
|
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/html-template.mjs
CHANGED
|
@@ -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 =
|
|
76
|
-
|
|
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
|
-
|
|
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="
|
|
234
|
-
<div
|
|
235
|
-
|
|
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" ? "⚠" : i.type === "tip" ? "✨" : i.type === "promote" ? "↑" : "ⓘ"}</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 →</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
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
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);
|