@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 +2 -2
- package/generate-dashboard.mjs +220 -27
- package/package.json +4 -3
- package/src/assembler.mjs +150 -0
- package/src/constants.mjs +1 -1
- package/src/demo.mjs +64 -1
- package/src/render.mjs +0 -4
- package/src/sections.mjs +413 -0
- package/src/watch.mjs +2 -2
- package/template/dashboard.css +1251 -0
- package/template/dashboard.html +51 -0
- package/template/dashboard.js +152 -0
- package/src/html-template.mjs +0 -814
package/README.md
CHANGED
|
@@ -31,9 +31,9 @@ Scans your home directory for git repos, collects Claude Code configuration (com
|
|
|
31
31
|
|
|
32
32
|

|
|
33
33
|
|
|
34
|
-
###
|
|
34
|
+
### Light mode
|
|
35
35
|
|
|
36
|
-

|
|
37
37
|
|
|
38
38
|
> Screenshots generated with `claude-code-dashboard --demo`
|
|
39
39
|
|
package/generate-dashboard.mjs
CHANGED
|
@@ -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/
|
|
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
|
-
|
|
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@viren/claude-code-dashboard",
|
|
3
|
-
"version": "0.0.
|
|
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
|
|
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
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/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>`);
|