claude-roi 0.2.4 → 0.3.1
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/package.json +1 -1
- package/src/dashboard.html +390 -63
- package/src/index.js +43 -40
- package/src/server.js +18 -1
package/package.json
CHANGED
package/src/dashboard.html
CHANGED
|
@@ -6,6 +6,15 @@
|
|
|
6
6
|
<title>Codelens AI — Agent Productivity Dashboard</title>
|
|
7
7
|
<link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,wght@0,300;0,400;0,500;0,600;0,700;1,400&family=JetBrains+Mono:wght@400;500;600;700;800&display=swap" rel="stylesheet">
|
|
8
8
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
9
|
+
<script>
|
|
10
|
+
(function() {
|
|
11
|
+
var pref = localStorage.getItem('codelens-theme') || 'system';
|
|
12
|
+
var resolved = pref === 'system'
|
|
13
|
+
? (window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark')
|
|
14
|
+
: pref;
|
|
15
|
+
if (resolved === 'light') document.documentElement.setAttribute('data-theme', 'light');
|
|
16
|
+
})();
|
|
17
|
+
</script>
|
|
9
18
|
<style>
|
|
10
19
|
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11
20
|
|
|
@@ -46,6 +55,86 @@
|
|
|
46
55
|
--radius-sm: 8px;
|
|
47
56
|
--font-display: 'JetBrains Mono', 'SF Mono', monospace;
|
|
48
57
|
--font-body: 'DM Sans', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
58
|
+
|
|
59
|
+
/* Glassmorphism & overlay tokens */
|
|
60
|
+
--glass-bg-from: rgba(21, 29, 43, 0.8);
|
|
61
|
+
--glass-bg-to: rgba(15, 23, 35, 0.6);
|
|
62
|
+
--glass-border: rgba(255, 255, 255, 0.05);
|
|
63
|
+
--glass-border-hover: rgba(255, 255, 255, 0.1);
|
|
64
|
+
--overlay-subtle: rgba(255, 255, 255, 0.01);
|
|
65
|
+
--overlay-light: rgba(255, 255, 255, 0.02);
|
|
66
|
+
--overlay-soft: rgba(255, 255, 255, 0.03);
|
|
67
|
+
--overlay-medium: rgba(255, 255, 255, 0.04);
|
|
68
|
+
--overlay-strong: rgba(255, 255, 255, 0.06);
|
|
69
|
+
--overlay-intense: rgba(255, 255, 255, 0.08);
|
|
70
|
+
--shadow-card: rgba(0, 0, 0, 0.3);
|
|
71
|
+
--shadow-card-hover: rgba(0, 0, 0, 0.4);
|
|
72
|
+
--shadow-medium: rgba(0, 0, 0, 0.2);
|
|
73
|
+
--shadow-elevated: rgba(0, 0, 0, 0.25);
|
|
74
|
+
--tooltip-bg: rgba(10, 14, 23, 0.95);
|
|
75
|
+
--tooltip-border: rgba(255, 255, 255, 0.06);
|
|
76
|
+
--noise-opacity: 0.015;
|
|
77
|
+
--funnel-text: rgba(255, 255, 255, 0.9);
|
|
78
|
+
|
|
79
|
+
/* Grade background tokens */
|
|
80
|
+
--grade-a-bg: rgba(34, 211, 168, 0.13);
|
|
81
|
+
--grade-b-bg: rgba(59, 130, 246, 0.13);
|
|
82
|
+
--grade-c-bg: rgba(245, 158, 11, 0.13);
|
|
83
|
+
--grade-d-bg: rgba(240, 136, 62, 0.13);
|
|
84
|
+
--grade-f-bg: rgba(239, 68, 68, 0.13);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/* ── Light Theme ─────────────────────────────── */
|
|
88
|
+
[data-theme="light"] {
|
|
89
|
+
--bg-primary: #f8fafc;
|
|
90
|
+
--bg-secondary: #f1f5f9;
|
|
91
|
+
--bg-card: #ffffff;
|
|
92
|
+
--bg-hover: #e2e8f0;
|
|
93
|
+
--bg-elevated: #f1f5f9;
|
|
94
|
+
--border: #cbd5e1;
|
|
95
|
+
--border-subtle: #e2e8f0;
|
|
96
|
+
--text-primary: #0f172a;
|
|
97
|
+
--text-secondary: #475569;
|
|
98
|
+
--text-muted: #64748b;
|
|
99
|
+
--accent-green: #0d9488;
|
|
100
|
+
--accent-red: #dc2626;
|
|
101
|
+
--accent-orange: #d97706;
|
|
102
|
+
--accent-blue: #2563eb;
|
|
103
|
+
--accent-purple: #7c3aed;
|
|
104
|
+
--accent-cyan: #0891b2;
|
|
105
|
+
--grade-a: #0d9488;
|
|
106
|
+
--grade-b: #2563eb;
|
|
107
|
+
--grade-c: #d97706;
|
|
108
|
+
--grade-d: #ea580c;
|
|
109
|
+
--grade-f: #dc2626;
|
|
110
|
+
--glow-green: rgba(13, 148, 136, 0.1);
|
|
111
|
+
--glow-blue: rgba(37, 99, 235, 0.1);
|
|
112
|
+
--glow-purple: rgba(124, 58, 237, 0.1);
|
|
113
|
+
--glow-orange: rgba(217, 119, 6, 0.1);
|
|
114
|
+
--glow-red: rgba(220, 38, 38, 0.1);
|
|
115
|
+
--glass-bg-from: rgba(255, 255, 255, 0.7);
|
|
116
|
+
--glass-bg-to: rgba(241, 245, 249, 0.5);
|
|
117
|
+
--glass-border: rgba(0, 0, 0, 0.06);
|
|
118
|
+
--glass-border-hover: rgba(0, 0, 0, 0.12);
|
|
119
|
+
--overlay-subtle: rgba(0, 0, 0, 0.01);
|
|
120
|
+
--overlay-light: rgba(0, 0, 0, 0.02);
|
|
121
|
+
--overlay-soft: rgba(0, 0, 0, 0.03);
|
|
122
|
+
--overlay-medium: rgba(0, 0, 0, 0.04);
|
|
123
|
+
--overlay-strong: rgba(0, 0, 0, 0.06);
|
|
124
|
+
--overlay-intense: rgba(0, 0, 0, 0.08);
|
|
125
|
+
--shadow-card: rgba(0, 0, 0, 0.06);
|
|
126
|
+
--shadow-card-hover: rgba(0, 0, 0, 0.1);
|
|
127
|
+
--shadow-medium: rgba(0, 0, 0, 0.08);
|
|
128
|
+
--shadow-elevated: rgba(0, 0, 0, 0.12);
|
|
129
|
+
--tooltip-bg: rgba(15, 23, 42, 0.92);
|
|
130
|
+
--tooltip-border: rgba(255, 255, 255, 0.1);
|
|
131
|
+
--noise-opacity: 0.008;
|
|
132
|
+
--funnel-text: rgba(0, 0, 0, 0.8);
|
|
133
|
+
--grade-a-bg: rgba(13, 148, 136, 0.1);
|
|
134
|
+
--grade-b-bg: rgba(37, 99, 235, 0.1);
|
|
135
|
+
--grade-c-bg: rgba(217, 119, 6, 0.1);
|
|
136
|
+
--grade-d-bg: rgba(234, 88, 12, 0.1);
|
|
137
|
+
--grade-f-bg: rgba(220, 38, 38, 0.1);
|
|
49
138
|
}
|
|
50
139
|
|
|
51
140
|
body {
|
|
@@ -65,7 +154,7 @@
|
|
|
65
154
|
inset: 0;
|
|
66
155
|
pointer-events: none;
|
|
67
156
|
z-index: 9999;
|
|
68
|
-
opacity:
|
|
157
|
+
opacity: var(--noise-opacity);
|
|
69
158
|
background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E");
|
|
70
159
|
background-repeat: repeat;
|
|
71
160
|
background-size: 256px 256px;
|
|
@@ -139,15 +228,15 @@
|
|
|
139
228
|
color: var(--text-muted);
|
|
140
229
|
}
|
|
141
230
|
.meta-info .badge {
|
|
142
|
-
background:
|
|
231
|
+
background: var(--overlay-medium);
|
|
143
232
|
backdrop-filter: blur(12px);
|
|
144
|
-
border: 1px solid
|
|
233
|
+
border: 1px solid var(--overlay-strong);
|
|
145
234
|
border-radius: 20px;
|
|
146
235
|
padding: 5px 14px;
|
|
147
236
|
transition: background 0.3s;
|
|
148
237
|
}
|
|
149
238
|
.meta-info .badge:hover {
|
|
150
|
-
background:
|
|
239
|
+
background: var(--overlay-intense);
|
|
151
240
|
}
|
|
152
241
|
|
|
153
242
|
/* Shared section heading style */
|
|
@@ -188,9 +277,9 @@
|
|
|
188
277
|
gap: 12px;
|
|
189
278
|
}
|
|
190
279
|
.stat-card {
|
|
191
|
-
background: linear-gradient(145deg,
|
|
280
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
192
281
|
backdrop-filter: blur(16px);
|
|
193
|
-
border: 1px solid
|
|
282
|
+
border: 1px solid var(--glass-border);
|
|
194
283
|
border-radius: var(--radius);
|
|
195
284
|
padding: 20px 24px;
|
|
196
285
|
display: flex;
|
|
@@ -205,9 +294,9 @@
|
|
|
205
294
|
}
|
|
206
295
|
.stat-card:hover {
|
|
207
296
|
transform: translateY(-3px);
|
|
208
|
-
border-color:
|
|
209
|
-
box-shadow: 0 8px 32px
|
|
210
|
-
0 0 0 1px
|
|
297
|
+
border-color: var(--glass-border-hover);
|
|
298
|
+
box-shadow: 0 8px 32px var(--shadow-card),
|
|
299
|
+
0 0 0 1px var(--glass-border);
|
|
211
300
|
}
|
|
212
301
|
.stat-card.glow::before {
|
|
213
302
|
content: '';
|
|
@@ -309,8 +398,8 @@
|
|
|
309
398
|
gap: 24px;
|
|
310
399
|
margin-top: 16px;
|
|
311
400
|
padding: 12px 20px;
|
|
312
|
-
background:
|
|
313
|
-
border: 1px solid
|
|
401
|
+
background: var(--overlay-light);
|
|
402
|
+
border: 1px solid var(--overlay-medium);
|
|
314
403
|
border-radius: var(--radius-sm);
|
|
315
404
|
}
|
|
316
405
|
.hero-legend span, .cost-legend span, .token-legend span {
|
|
@@ -350,15 +439,15 @@
|
|
|
350
439
|
align-items: flex-start;
|
|
351
440
|
gap: 10px;
|
|
352
441
|
padding: 14px 18px;
|
|
353
|
-
background:
|
|
354
|
-
border: 1px solid
|
|
442
|
+
background: var(--overlay-light);
|
|
443
|
+
border: 1px solid var(--overlay-medium);
|
|
355
444
|
border-radius: var(--radius-sm);
|
|
356
445
|
font-size: 0.88rem;
|
|
357
446
|
transition: background 0.2s, border-color 0.2s, transform 0.2s;
|
|
358
447
|
}
|
|
359
448
|
.insight:hover {
|
|
360
|
-
background:
|
|
361
|
-
border-color:
|
|
449
|
+
background: var(--overlay-medium);
|
|
450
|
+
border-color: var(--overlay-intense);
|
|
362
451
|
transform: translateX(4px);
|
|
363
452
|
}
|
|
364
453
|
.insight .icon { font-size: 1.1rem; flex-shrink: 0; }
|
|
@@ -375,16 +464,16 @@
|
|
|
375
464
|
margin-bottom: 48px;
|
|
376
465
|
}
|
|
377
466
|
.chart-card {
|
|
378
|
-
background: linear-gradient(145deg,
|
|
467
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
379
468
|
backdrop-filter: blur(16px);
|
|
380
|
-
border: 1px solid
|
|
469
|
+
border: 1px solid var(--glass-border);
|
|
381
470
|
border-radius: var(--radius);
|
|
382
471
|
padding: 24px;
|
|
383
472
|
transition: border-color 0.3s, box-shadow 0.3s;
|
|
384
473
|
}
|
|
385
474
|
.chart-card:hover {
|
|
386
|
-
border-color:
|
|
387
|
-
box-shadow: 0 4px 20px
|
|
475
|
+
border-color: var(--overlay-intense);
|
|
476
|
+
box-shadow: 0 4px 20px var(--shadow-medium);
|
|
388
477
|
}
|
|
389
478
|
.chart-card.full-width {
|
|
390
479
|
grid-column: 1 / -1;
|
|
@@ -407,8 +496,8 @@
|
|
|
407
496
|
.chart-header h3 { margin-bottom: 0; }
|
|
408
497
|
.scale-toggle {
|
|
409
498
|
padding: 5px 12px;
|
|
410
|
-
border: 1px solid
|
|
411
|
-
background:
|
|
499
|
+
border: 1px solid var(--overlay-intense);
|
|
500
|
+
background: var(--overlay-soft);
|
|
412
501
|
color: var(--text-secondary);
|
|
413
502
|
border-radius: 6px;
|
|
414
503
|
cursor: pointer;
|
|
@@ -420,7 +509,7 @@
|
|
|
420
509
|
.scale-toggle:hover {
|
|
421
510
|
color: var(--text-primary);
|
|
422
511
|
border-color: var(--accent-blue);
|
|
423
|
-
background:
|
|
512
|
+
background: var(--glow-blue);
|
|
424
513
|
}
|
|
425
514
|
.chart-container {
|
|
426
515
|
position: relative;
|
|
@@ -433,9 +522,9 @@
|
|
|
433
522
|
margin-bottom: 48px;
|
|
434
523
|
}
|
|
435
524
|
.survival-card {
|
|
436
|
-
background: linear-gradient(145deg,
|
|
525
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
437
526
|
backdrop-filter: blur(16px);
|
|
438
|
-
border: 1px solid
|
|
527
|
+
border: 1px solid var(--glass-border);
|
|
439
528
|
border-radius: var(--radius);
|
|
440
529
|
padding: 24px;
|
|
441
530
|
}
|
|
@@ -458,7 +547,7 @@
|
|
|
458
547
|
width: 16px;
|
|
459
548
|
height: 16px;
|
|
460
549
|
border-radius: 50%;
|
|
461
|
-
background:
|
|
550
|
+
background: var(--glass-border);
|
|
462
551
|
color: var(--text-muted);
|
|
463
552
|
font-size: 0.6rem;
|
|
464
553
|
font-style: normal;
|
|
@@ -470,7 +559,7 @@
|
|
|
470
559
|
transition: background 0.2s, color 0.2s;
|
|
471
560
|
}
|
|
472
561
|
.info-tip:hover {
|
|
473
|
-
background:
|
|
562
|
+
background: var(--glow-blue);
|
|
474
563
|
color: var(--accent-blue);
|
|
475
564
|
}
|
|
476
565
|
@keyframes tooltipFade {
|
|
@@ -483,7 +572,7 @@
|
|
|
483
572
|
bottom: calc(100% + 10px);
|
|
484
573
|
left: 50%;
|
|
485
574
|
transform: translateX(-50%) translateY(0);
|
|
486
|
-
background:
|
|
575
|
+
background: var(--tooltip-bg);
|
|
487
576
|
backdrop-filter: blur(16px);
|
|
488
577
|
color: var(--text-secondary);
|
|
489
578
|
padding: 10px 14px;
|
|
@@ -492,10 +581,10 @@
|
|
|
492
581
|
font-weight: 400;
|
|
493
582
|
width: 280px;
|
|
494
583
|
line-height: 1.5;
|
|
495
|
-
border: 1px solid
|
|
584
|
+
border: 1px solid var(--tooltip-border);
|
|
496
585
|
z-index: 100;
|
|
497
586
|
pointer-events: none;
|
|
498
|
-
box-shadow: 0 8px 32px
|
|
587
|
+
box-shadow: 0 8px 32px var(--shadow-card-hover);
|
|
499
588
|
white-space: normal;
|
|
500
589
|
text-transform: none;
|
|
501
590
|
letter-spacing: normal;
|
|
@@ -508,13 +597,13 @@
|
|
|
508
597
|
}
|
|
509
598
|
/* Make table header tooltip icons more visible */
|
|
510
599
|
thead .info-tip {
|
|
511
|
-
background:
|
|
600
|
+
background: var(--glass-border-hover);
|
|
512
601
|
color: var(--text-secondary);
|
|
513
602
|
}
|
|
514
603
|
.survival-bar {
|
|
515
604
|
height: 24px;
|
|
516
|
-
background:
|
|
517
|
-
border: 1px solid
|
|
605
|
+
background: var(--overlay-medium);
|
|
606
|
+
border: 1px solid var(--overlay-strong);
|
|
518
607
|
border-radius: 12px;
|
|
519
608
|
overflow: hidden;
|
|
520
609
|
margin-bottom: 12px;
|
|
@@ -586,9 +675,9 @@
|
|
|
586
675
|
letter-spacing: 0.12em;
|
|
587
676
|
}
|
|
588
677
|
.sessions-table-wrap {
|
|
589
|
-
background: linear-gradient(145deg,
|
|
678
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
590
679
|
backdrop-filter: blur(16px);
|
|
591
|
-
border: 1px solid
|
|
680
|
+
border: 1px solid var(--glass-border);
|
|
592
681
|
border-radius: var(--radius);
|
|
593
682
|
overflow-x: auto;
|
|
594
683
|
overflow-y: visible;
|
|
@@ -607,7 +696,7 @@
|
|
|
607
696
|
font-size: 0.75rem;
|
|
608
697
|
text-transform: uppercase;
|
|
609
698
|
letter-spacing: 0.08em;
|
|
610
|
-
border-bottom: 1px solid
|
|
699
|
+
border-bottom: 1px solid var(--overlay-strong);
|
|
611
700
|
cursor: pointer;
|
|
612
701
|
user-select: none;
|
|
613
702
|
white-space: nowrap;
|
|
@@ -626,10 +715,10 @@
|
|
|
626
715
|
background: var(--accent-blue);
|
|
627
716
|
}
|
|
628
717
|
tbody tr {
|
|
629
|
-
border-bottom: 1px solid
|
|
718
|
+
border-bottom: 1px solid var(--overlay-soft);
|
|
630
719
|
transition: background 0.2s;
|
|
631
720
|
}
|
|
632
|
-
tbody tr:hover { background:
|
|
721
|
+
tbody tr:hover { background: var(--overlay-soft); }
|
|
633
722
|
tbody tr:last-child { border-bottom: none; }
|
|
634
723
|
tbody td {
|
|
635
724
|
padding: 12px 14px;
|
|
@@ -654,7 +743,7 @@
|
|
|
654
743
|
tr.orphaned { background: rgba(245, 158, 11, 0.06); }
|
|
655
744
|
.expand-row {
|
|
656
745
|
display: none;
|
|
657
|
-
background:
|
|
746
|
+
background: var(--overlay-subtle);
|
|
658
747
|
}
|
|
659
748
|
.expand-row.open { display: table-row; animation: fadeSlideUp 0.3s ease forwards; }
|
|
660
749
|
.expand-row td {
|
|
@@ -704,8 +793,8 @@
|
|
|
704
793
|
}
|
|
705
794
|
.pagination button {
|
|
706
795
|
padding: 8px 18px;
|
|
707
|
-
border: 1px solid
|
|
708
|
-
background:
|
|
796
|
+
border: 1px solid var(--overlay-intense);
|
|
797
|
+
background: var(--overlay-soft);
|
|
709
798
|
color: var(--text-primary);
|
|
710
799
|
border-radius: 8px;
|
|
711
800
|
cursor: pointer;
|
|
@@ -714,9 +803,9 @@
|
|
|
714
803
|
transition: all 0.2s;
|
|
715
804
|
}
|
|
716
805
|
.pagination button:hover:not(:disabled) {
|
|
717
|
-
background:
|
|
806
|
+
background: var(--overlay-intense);
|
|
718
807
|
border-color: var(--accent-blue);
|
|
719
|
-
box-shadow: 0 0 12px
|
|
808
|
+
box-shadow: 0 0 12px var(--glow-blue);
|
|
720
809
|
}
|
|
721
810
|
.pagination button:disabled { opacity: 0.4; cursor: default; }
|
|
722
811
|
.pagination span { font-size: 0.8rem; color: var(--text-muted); }
|
|
@@ -768,9 +857,9 @@
|
|
|
768
857
|
gap: 16px;
|
|
769
858
|
}
|
|
770
859
|
.token-stat-card {
|
|
771
|
-
background: linear-gradient(145deg,
|
|
860
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
772
861
|
backdrop-filter: blur(16px);
|
|
773
|
-
border: 1px solid
|
|
862
|
+
border: 1px solid var(--glass-border);
|
|
774
863
|
border-radius: var(--radius);
|
|
775
864
|
padding: 20px 24px;
|
|
776
865
|
text-align: center;
|
|
@@ -782,7 +871,7 @@
|
|
|
782
871
|
}
|
|
783
872
|
.token-stat-card:hover {
|
|
784
873
|
transform: translateY(-3px);
|
|
785
|
-
box-shadow: 0 8px 32px
|
|
874
|
+
box-shadow: 0 8px 32px var(--shadow-elevated);
|
|
786
875
|
}
|
|
787
876
|
.token-stat-card.burned { border-top-color: var(--accent-orange); }
|
|
788
877
|
.token-stat-card.wasted { border-top-color: var(--accent-red); }
|
|
@@ -906,9 +995,9 @@
|
|
|
906
995
|
display: grid;
|
|
907
996
|
grid-template-columns: repeat(4, 1fr);
|
|
908
997
|
gap: 0;
|
|
909
|
-
background: linear-gradient(145deg,
|
|
998
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
910
999
|
backdrop-filter: blur(16px);
|
|
911
|
-
border: 1px solid
|
|
1000
|
+
border: 1px solid var(--glass-border);
|
|
912
1001
|
border-radius: var(--radius);
|
|
913
1002
|
overflow: hidden;
|
|
914
1003
|
position: relative;
|
|
@@ -933,7 +1022,7 @@
|
|
|
933
1022
|
position: relative;
|
|
934
1023
|
}
|
|
935
1024
|
.period-card + .period-card {
|
|
936
|
-
border-left: 1px solid
|
|
1025
|
+
border-left: 1px solid var(--glass-border);
|
|
937
1026
|
}
|
|
938
1027
|
.period-card:hover {
|
|
939
1028
|
background: var(--bg-hover);
|
|
@@ -989,9 +1078,9 @@
|
|
|
989
1078
|
margin-bottom: 48px;
|
|
990
1079
|
}
|
|
991
1080
|
.token-funnel-card {
|
|
992
|
-
background: linear-gradient(145deg,
|
|
1081
|
+
background: linear-gradient(145deg, var(--glass-bg-from), var(--glass-bg-to));
|
|
993
1082
|
backdrop-filter: blur(16px);
|
|
994
|
-
border: 1px solid
|
|
1083
|
+
border: 1px solid var(--glass-border);
|
|
995
1084
|
border-radius: var(--radius);
|
|
996
1085
|
padding: 24px;
|
|
997
1086
|
}
|
|
@@ -1030,7 +1119,7 @@
|
|
|
1030
1119
|
justify-content: center;
|
|
1031
1120
|
font-size: 0.7rem;
|
|
1032
1121
|
font-weight: 500;
|
|
1033
|
-
color:
|
|
1122
|
+
color: var(--funnel-text);
|
|
1034
1123
|
overflow: hidden;
|
|
1035
1124
|
white-space: nowrap;
|
|
1036
1125
|
}
|
|
@@ -1104,6 +1193,112 @@
|
|
|
1104
1193
|
animation: gradeReveal 1.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) forwards;
|
|
1105
1194
|
}
|
|
1106
1195
|
|
|
1196
|
+
/* ── Header Actions (top-right buttons) ─────── */
|
|
1197
|
+
.header-actions {
|
|
1198
|
+
position: absolute;
|
|
1199
|
+
top: 16px;
|
|
1200
|
+
right: 16px;
|
|
1201
|
+
z-index: 10;
|
|
1202
|
+
display: flex;
|
|
1203
|
+
align-items: center;
|
|
1204
|
+
gap: 8px;
|
|
1205
|
+
}
|
|
1206
|
+
.header-action-btn {
|
|
1207
|
+
width: 42px;
|
|
1208
|
+
height: 42px;
|
|
1209
|
+
border-radius: 50%;
|
|
1210
|
+
border: 1px solid var(--glass-border);
|
|
1211
|
+
background: var(--overlay-medium);
|
|
1212
|
+
backdrop-filter: blur(12px);
|
|
1213
|
+
color: var(--text-secondary);
|
|
1214
|
+
cursor: pointer;
|
|
1215
|
+
display: flex;
|
|
1216
|
+
align-items: center;
|
|
1217
|
+
justify-content: center;
|
|
1218
|
+
transition: background 0.3s, border-color 0.3s, color 0.3s, transform 0.2s;
|
|
1219
|
+
}
|
|
1220
|
+
.header-action-btn:hover {
|
|
1221
|
+
background: var(--overlay-intense);
|
|
1222
|
+
border-color: var(--glass-border-hover);
|
|
1223
|
+
color: var(--text-primary);
|
|
1224
|
+
transform: scale(1.1);
|
|
1225
|
+
}
|
|
1226
|
+
.header-action-btn svg {
|
|
1227
|
+
width: 18px;
|
|
1228
|
+
height: 18px;
|
|
1229
|
+
}
|
|
1230
|
+
.refresh-btn.refreshing svg {
|
|
1231
|
+
animation: spin 0.8s linear infinite;
|
|
1232
|
+
}
|
|
1233
|
+
.theme-switcher {
|
|
1234
|
+
display: flex;
|
|
1235
|
+
align-items: center;
|
|
1236
|
+
border-radius: 22px;
|
|
1237
|
+
border: 1px solid var(--glass-border);
|
|
1238
|
+
background: var(--overlay-medium);
|
|
1239
|
+
backdrop-filter: blur(12px);
|
|
1240
|
+
padding: 3px;
|
|
1241
|
+
gap: 2px;
|
|
1242
|
+
}
|
|
1243
|
+
.theme-switcher button {
|
|
1244
|
+
width: 34px;
|
|
1245
|
+
height: 34px;
|
|
1246
|
+
border-radius: 50%;
|
|
1247
|
+
border: none;
|
|
1248
|
+
background: transparent;
|
|
1249
|
+
color: var(--text-muted);
|
|
1250
|
+
cursor: pointer;
|
|
1251
|
+
display: flex;
|
|
1252
|
+
align-items: center;
|
|
1253
|
+
justify-content: center;
|
|
1254
|
+
transition: background 0.25s, color 0.25s, transform 0.15s;
|
|
1255
|
+
}
|
|
1256
|
+
.theme-switcher button:hover {
|
|
1257
|
+
color: var(--text-primary);
|
|
1258
|
+
transform: scale(1.08);
|
|
1259
|
+
}
|
|
1260
|
+
.theme-switcher button.active {
|
|
1261
|
+
background: var(--overlay-intense);
|
|
1262
|
+
color: var(--text-primary);
|
|
1263
|
+
box-shadow: 0 1px 4px var(--shadow-card);
|
|
1264
|
+
}
|
|
1265
|
+
.theme-switcher button svg {
|
|
1266
|
+
width: 16px;
|
|
1267
|
+
height: 16px;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
/* ── Light Theme Overrides ───────────────────── */
|
|
1271
|
+
[data-theme="light"] body {
|
|
1272
|
+
background:
|
|
1273
|
+
radial-gradient(ellipse 80% 50% at 50% -20%, rgba(37, 99, 235, 0.05), transparent),
|
|
1274
|
+
radial-gradient(ellipse 60% 40% at 100% 50%, rgba(124, 58, 237, 0.04), transparent),
|
|
1275
|
+
radial-gradient(ellipse 50% 30% at 0% 80%, rgba(13, 148, 136, 0.04), transparent),
|
|
1276
|
+
var(--bg-primary);
|
|
1277
|
+
}
|
|
1278
|
+
[data-theme="light"] header::before {
|
|
1279
|
+
background:
|
|
1280
|
+
radial-gradient(ellipse 60% 50% at 20% 50%, rgba(37, 99, 235, 0.07), transparent),
|
|
1281
|
+
radial-gradient(ellipse 50% 60% at 80% 30%, rgba(124, 58, 237, 0.05), transparent),
|
|
1282
|
+
radial-gradient(ellipse 40% 40% at 50% 80%, rgba(13, 148, 136, 0.04), transparent);
|
|
1283
|
+
}
|
|
1284
|
+
[data-theme="light"] .info-tip:hover::after {
|
|
1285
|
+
color: #e2e8f0;
|
|
1286
|
+
}
|
|
1287
|
+
[data-theme="light"] tr.orphaned {
|
|
1288
|
+
background: rgba(217, 119, 6, 0.06);
|
|
1289
|
+
}
|
|
1290
|
+
[data-theme="light"] .main-badge {
|
|
1291
|
+
background: rgba(13, 148, 136, 0.12);
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
/* Smooth theme transition */
|
|
1295
|
+
html.theme-transition,
|
|
1296
|
+
html.theme-transition *,
|
|
1297
|
+
html.theme-transition *::before,
|
|
1298
|
+
html.theme-transition *::after {
|
|
1299
|
+
transition: background-color 0.3s ease, color 0.3s ease, border-color 0.3s ease, box-shadow 0.3s ease, background 0.3s ease !important;
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1107
1302
|
/* Responsive */
|
|
1108
1303
|
@media (max-width: 900px) {
|
|
1109
1304
|
.hero-stats { grid-template-columns: 1fr; }
|
|
@@ -1112,7 +1307,7 @@
|
|
|
1112
1307
|
.period-stats { grid-template-columns: repeat(2, 1fr); }
|
|
1113
1308
|
.period-stats::before { display: none; }
|
|
1114
1309
|
.period-card:nth-child(3) { border-left: none; }
|
|
1115
|
-
.period-card:nth-child(3), .period-card:nth-child(4) { border-top: 1px solid
|
|
1310
|
+
.period-card:nth-child(3), .period-card:nth-child(4) { border-top: 1px solid var(--glass-border); }
|
|
1116
1311
|
.charts-grid { grid-template-columns: 1fr; }
|
|
1117
1312
|
}
|
|
1118
1313
|
@media (max-width: 600px) {
|
|
@@ -1128,6 +1323,30 @@
|
|
|
1128
1323
|
<body>
|
|
1129
1324
|
<div class="container">
|
|
1130
1325
|
<header>
|
|
1326
|
+
<div class="header-actions">
|
|
1327
|
+
<button class="header-action-btn refresh-btn" id="refresh-btn" aria-label="Refresh data" title="Refresh dashboard data">
|
|
1328
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1329
|
+
<polyline points="23 4 23 10 17 10"/><polyline points="1 20 1 14 7 14"/><path d="M3.51 9a9 9 0 0 1 14.85-3.36L23 10M1 14l4.64 4.36A9 9 0 0 0 20.49 15"/>
|
|
1330
|
+
</svg>
|
|
1331
|
+
</button>
|
|
1332
|
+
<div class="theme-switcher" id="theme-switcher">
|
|
1333
|
+
<button data-theme-pref="system" title="System theme" aria-label="Use system theme">
|
|
1334
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1335
|
+
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"/><line x1="8" y1="21" x2="16" y2="21"/><line x1="12" y1="17" x2="12" y2="21"/>
|
|
1336
|
+
</svg>
|
|
1337
|
+
</button>
|
|
1338
|
+
<button data-theme-pref="dark" title="Dark theme" aria-label="Dark theme">
|
|
1339
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1340
|
+
<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>
|
|
1341
|
+
</svg>
|
|
1342
|
+
</button>
|
|
1343
|
+
<button data-theme-pref="light" title="Light theme" aria-label="Light theme">
|
|
1344
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
1345
|
+
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
1346
|
+
</svg>
|
|
1347
|
+
</button>
|
|
1348
|
+
</div>
|
|
1349
|
+
</div>
|
|
1131
1350
|
<h1>Codelens AI</h1>
|
|
1132
1351
|
<p class="tagline">Correlates your AI coding agent's token spend with actual git output — see what shipped, what churned, and what it cost.</p>
|
|
1133
1352
|
<div class="meta-info">
|
|
@@ -1151,12 +1370,107 @@
|
|
|
1151
1370
|
</div>
|
|
1152
1371
|
|
|
1153
1372
|
<script>
|
|
1154
|
-
const
|
|
1373
|
+
const GRADE_VAR = { A: 'var(--grade-a)', B: 'var(--grade-b)', C: 'var(--grade-c)', D: 'var(--grade-d)', F: 'var(--grade-f)' };
|
|
1374
|
+
const GRADE_BG_VAR = { A: 'var(--grade-a-bg)', B: 'var(--grade-b-bg)', C: 'var(--grade-c-bg)', D: 'var(--grade-d-bg)', F: 'var(--grade-f-bg)' };
|
|
1155
1375
|
const INSIGHT_ICONS = { warning: '!', success: '+', info: 'i', tip: '*' };
|
|
1156
1376
|
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
1157
1377
|
let DATA = null;
|
|
1158
1378
|
let initialRenderDone = false;
|
|
1159
1379
|
|
|
1380
|
+
function getThemePref() {
|
|
1381
|
+
return localStorage.getItem('codelens-theme') || 'system';
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function resolveTheme(pref) {
|
|
1385
|
+
if (pref === 'system') return window.matchMedia('(prefers-color-scheme: light)').matches ? 'light' : 'dark';
|
|
1386
|
+
return pref;
|
|
1387
|
+
}
|
|
1388
|
+
|
|
1389
|
+
function getTheme() {
|
|
1390
|
+
return document.documentElement.getAttribute('data-theme') || 'dark';
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
function applyTheme(pref) {
|
|
1394
|
+
const resolved = resolveTheme(pref);
|
|
1395
|
+
document.documentElement.classList.add('theme-transition');
|
|
1396
|
+
if (resolved === 'light') {
|
|
1397
|
+
document.documentElement.setAttribute('data-theme', 'light');
|
|
1398
|
+
} else {
|
|
1399
|
+
document.documentElement.removeAttribute('data-theme');
|
|
1400
|
+
}
|
|
1401
|
+
localStorage.setItem('codelens-theme', pref);
|
|
1402
|
+
updateChartTheme(resolved);
|
|
1403
|
+
updateThemeSwitcher(pref);
|
|
1404
|
+
setTimeout(() => document.documentElement.classList.remove('theme-transition'), 350);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function updateThemeSwitcher(pref) {
|
|
1408
|
+
const switcher = document.getElementById('theme-switcher');
|
|
1409
|
+
if (!switcher) return;
|
|
1410
|
+
switcher.querySelectorAll('button').forEach(btn => {
|
|
1411
|
+
btn.classList.toggle('active', btn.dataset.themePref === pref);
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
function updateChartTheme(theme) {
|
|
1416
|
+
const isLight = theme === 'light';
|
|
1417
|
+
Chart.defaults.color = isLight ? '#475569' : '#94a3b8';
|
|
1418
|
+
Chart.defaults.borderColor = isLight ? '#cbd5e1' : '#1f2d3d';
|
|
1419
|
+
Chart.defaults.plugins.tooltip.backgroundColor = isLight ? 'rgba(15, 23, 42, 0.92)' : 'rgba(10, 14, 23, 0.95)';
|
|
1420
|
+
Chart.defaults.plugins.tooltip.borderColor = isLight ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.08)';
|
|
1421
|
+
Object.values(Chart.instances || {}).forEach(chart => {
|
|
1422
|
+
if (chart.options.scales) {
|
|
1423
|
+
Object.values(chart.options.scales).forEach(scale => {
|
|
1424
|
+
if (scale.grid) scale.grid.color = isLight ? '#e2e8f0' : '#1f2d3d';
|
|
1425
|
+
if (scale.ticks) scale.ticks.color = isLight ? '#475569' : '#94a3b8';
|
|
1426
|
+
if (scale.title) scale.title.color = isLight ? '#475569' : '#94a3b8';
|
|
1427
|
+
});
|
|
1428
|
+
}
|
|
1429
|
+
chart.update('none');
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
function initTheme() {
|
|
1434
|
+
const switcher = document.getElementById('theme-switcher');
|
|
1435
|
+
if (!switcher) return;
|
|
1436
|
+
const pref = getThemePref();
|
|
1437
|
+
updateThemeSwitcher(pref);
|
|
1438
|
+
switcher.addEventListener('click', (e) => {
|
|
1439
|
+
const btn = e.target.closest('button[data-theme-pref]');
|
|
1440
|
+
if (!btn) return;
|
|
1441
|
+
applyTheme(btn.dataset.themePref);
|
|
1442
|
+
});
|
|
1443
|
+
window.matchMedia('(prefers-color-scheme: light)').addEventListener('change', () => {
|
|
1444
|
+
if (getThemePref() === 'system') applyTheme('system');
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
async function refreshData() {
|
|
1449
|
+
const btn = document.getElementById('refresh-btn');
|
|
1450
|
+
if (!btn || btn.classList.contains('refreshing')) return;
|
|
1451
|
+
btn.classList.add('refreshing');
|
|
1452
|
+
try {
|
|
1453
|
+
// Trigger server-side re-parse: clears cache, re-reads sessions, re-analyzes git, recomputes metrics
|
|
1454
|
+
const refreshRes = await fetch('/api/refresh', { method: 'POST' });
|
|
1455
|
+
if (!refreshRes.ok) {
|
|
1456
|
+
const err = await refreshRes.json().catch(() => ({}));
|
|
1457
|
+
throw new Error(err.error || 'Refresh failed');
|
|
1458
|
+
}
|
|
1459
|
+
// Fetch the newly computed payload
|
|
1460
|
+
const res = await fetch('/api/all');
|
|
1461
|
+
DATA = await res.json();
|
|
1462
|
+
initialRenderDone = false;
|
|
1463
|
+
render();
|
|
1464
|
+
} catch (err) {
|
|
1465
|
+
document.getElementById('app').innerHTML = `
|
|
1466
|
+
<div class="loading" style="color: var(--accent-red);">
|
|
1467
|
+
Failed to refresh: ${err.message}
|
|
1468
|
+
</div>`;
|
|
1469
|
+
} finally {
|
|
1470
|
+
btn.classList.remove('refreshing');
|
|
1471
|
+
}
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1160
1474
|
function formatTokens(n) {
|
|
1161
1475
|
if (n >= 1_000_000_000) return (n / 1_000_000_000).toFixed(1) + 'B';
|
|
1162
1476
|
if (n >= 1_000_000) return (n / 1_000_000).toFixed(1) + 'M';
|
|
@@ -1185,6 +1499,9 @@ let timelineChart = null;
|
|
|
1185
1499
|
let timelineLogScale = false;
|
|
1186
1500
|
|
|
1187
1501
|
document.addEventListener('DOMContentLoaded', async () => {
|
|
1502
|
+
initTheme();
|
|
1503
|
+
const refreshBtn = document.getElementById('refresh-btn');
|
|
1504
|
+
if (refreshBtn) refreshBtn.addEventListener('click', refreshData);
|
|
1188
1505
|
try {
|
|
1189
1506
|
const res = await fetch('/api/all');
|
|
1190
1507
|
DATA = await res.json();
|
|
@@ -1257,7 +1574,7 @@ function render() {
|
|
|
1257
1574
|
}
|
|
1258
1575
|
|
|
1259
1576
|
function renderHeroStats(s) {
|
|
1260
|
-
const gradeColor =
|
|
1577
|
+
const gradeColor = GRADE_VAR[s.overallGrade] || GRADE_VAR.F;
|
|
1261
1578
|
const GRADE_DEG = { A: 324, B: 270, C: 216, D: 144, F: 72 };
|
|
1262
1579
|
const gradeDeg = GRADE_DEG[s.overallGrade] || 72;
|
|
1263
1580
|
return `<div class="stats-section">
|
|
@@ -1592,7 +1909,8 @@ function renderSessionsTable(sessions) {
|
|
|
1592
1909
|
<tbody>
|
|
1593
1910
|
${pageData.map((s, i) => {
|
|
1594
1911
|
const idx = (page - 1) * pageSize + i;
|
|
1595
|
-
const gradeColor =
|
|
1912
|
+
const gradeColor = GRADE_VAR[s.grade] || GRADE_VAR.F;
|
|
1913
|
+
const gradeBg = GRADE_BG_VAR[s.grade] || GRADE_BG_VAR.F;
|
|
1596
1914
|
const primaryName = formatModelName(s.model || 'unknown');
|
|
1597
1915
|
const subModels = Object.keys(s.modelBreakdown || {})
|
|
1598
1916
|
.filter(m => m !== s.model)
|
|
@@ -1610,8 +1928,8 @@ function renderSessionsTable(sessions) {
|
|
|
1610
1928
|
<td>${s.userMessageCount + s.assistantMessageCount}</td>
|
|
1611
1929
|
<td>$${s.cost.totalCost.toFixed(2)}</td>
|
|
1612
1930
|
<td>${s.commitCount}</td>
|
|
1613
|
-
<td><span style="color
|
|
1614
|
-
<td><span class="grade-badge" style="background:${
|
|
1931
|
+
<td><span style="color:var(--accent-green)">+${s.linesAdded.toLocaleString()}</span> / <span style="color:var(--accent-red)">-${s.linesDeleted.toLocaleString()}</span></td>
|
|
1932
|
+
<td><span class="grade-badge" style="background:${gradeBg};color:${gradeColor};">${s.grade}</span></td>
|
|
1615
1933
|
</tr>
|
|
1616
1934
|
<tr class="expand-row" id="expand-${idx}">
|
|
1617
1935
|
<td colspan="8">
|
|
@@ -1664,11 +1982,12 @@ function formatDate(iso) {
|
|
|
1664
1982
|
}
|
|
1665
1983
|
|
|
1666
1984
|
function initCharts() {
|
|
1667
|
-
|
|
1668
|
-
Chart.defaults.
|
|
1985
|
+
const isLight = getTheme() === 'light';
|
|
1986
|
+
Chart.defaults.color = isLight ? '#475569' : '#94a3b8';
|
|
1987
|
+
Chart.defaults.borderColor = isLight ? '#cbd5e1' : '#1f2d3d';
|
|
1669
1988
|
Chart.defaults.font.family = "'DM Sans', sans-serif";
|
|
1670
|
-
Chart.defaults.plugins.tooltip.backgroundColor = 'rgba(10, 14, 23, 0.95)';
|
|
1671
|
-
Chart.defaults.plugins.tooltip.borderColor = 'rgba(255, 255, 255, 0.08)';
|
|
1989
|
+
Chart.defaults.plugins.tooltip.backgroundColor = isLight ? 'rgba(15, 23, 42, 0.92)' : 'rgba(10, 14, 23, 0.95)';
|
|
1990
|
+
Chart.defaults.plugins.tooltip.borderColor = isLight ? 'rgba(255, 255, 255, 0.1)' : 'rgba(255, 255, 255, 0.08)';
|
|
1672
1991
|
Chart.defaults.plugins.tooltip.borderWidth = 1;
|
|
1673
1992
|
Chart.defaults.plugins.tooltip.cornerRadius = 8;
|
|
1674
1993
|
Chart.defaults.plugins.tooltip.titleFont = { family: "'JetBrains Mono', monospace", size: 12, weight: '600' };
|
|
@@ -2018,14 +2337,22 @@ window.sortTable = function(col) {
|
|
|
2018
2337
|
if (sortCol === col) { sortOrder *= -1; }
|
|
2019
2338
|
else { sortCol = col; sortOrder = -1; }
|
|
2020
2339
|
currentPage = 1;
|
|
2021
|
-
|
|
2340
|
+
rerenderSessionsTable();
|
|
2022
2341
|
};
|
|
2023
2342
|
|
|
2024
2343
|
window.changePage = function(delta) {
|
|
2025
2344
|
currentPage += delta;
|
|
2026
|
-
|
|
2345
|
+
rerenderSessionsTable();
|
|
2027
2346
|
};
|
|
2028
2347
|
|
|
2348
|
+
function rerenderSessionsTable() {
|
|
2349
|
+
const container = document.querySelector('.sessions-section');
|
|
2350
|
+
if (container) {
|
|
2351
|
+
const wrapper = container.parentElement;
|
|
2352
|
+
wrapper.innerHTML = renderSessionsTable(DATA.sessions);
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
|
|
2029
2356
|
window.toggleTimelineScale = function() {
|
|
2030
2357
|
if (!timelineChart) return;
|
|
2031
2358
|
timelineLogScale = !timelineLogScale;
|
package/src/index.js
CHANGED
|
@@ -15,42 +15,20 @@ const { version: VERSION } = JSON.parse(
|
|
|
15
15
|
readFileSync(new URL('../package.json', import.meta.url), 'utf8')
|
|
16
16
|
);
|
|
17
17
|
|
|
18
|
-
async function
|
|
19
|
-
const program = new Command();
|
|
20
|
-
program
|
|
21
|
-
.name('claude-roi')
|
|
22
|
-
.description('Correlate Claude Code token usage with git output to measure AI coding agent ROI')
|
|
23
|
-
.version(VERSION)
|
|
24
|
-
.option('-p, --port <number>', 'port to serve dashboard', '3457')
|
|
25
|
-
.option('-d, --days <number>', 'number of days to look back', '30')
|
|
26
|
-
.option('--no-open', 'do not auto-open browser')
|
|
27
|
-
.option('--json', 'output raw JSON to stdout instead of starting server')
|
|
28
|
-
.option('--project <name>', 'filter to specific project')
|
|
29
|
-
.option('--refresh', 'force full re-parse, ignore cache');
|
|
30
|
-
|
|
31
|
-
program.parse();
|
|
32
|
-
const opts = program.opts();
|
|
33
|
-
const port = parseInt(opts.port, 10);
|
|
34
|
-
const days = parseInt(opts.days, 10);
|
|
35
|
-
|
|
36
|
-
console.log(`\x1b[36mclaude-roi\x1b[0m v${VERSION}`);
|
|
37
|
-
|
|
38
|
-
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
39
|
-
|
|
18
|
+
async function buildPayload(claudeDir, days, project, forceRefresh = false) {
|
|
40
19
|
// Step 1: Parse sessions (with caching)
|
|
41
20
|
let sessions;
|
|
42
21
|
let fileIndex;
|
|
43
22
|
const startParse = Date.now();
|
|
44
23
|
|
|
45
|
-
if (
|
|
24
|
+
if (forceRefresh) {
|
|
46
25
|
deleteCache();
|
|
47
26
|
console.log('Cache cleared, performing full parse...');
|
|
48
27
|
}
|
|
49
28
|
|
|
50
|
-
const cached =
|
|
29
|
+
const cached = forceRefresh ? null : loadCache();
|
|
51
30
|
|
|
52
31
|
if (cached) {
|
|
53
|
-
// Incremental parse: only process new/modified files
|
|
54
32
|
const stale = getStaleFiles(claudeDir, cached.fileIndex);
|
|
55
33
|
const newCount = stale.newFiles.length;
|
|
56
34
|
const modifiedCount = stale.modifiedFiles.length;
|
|
@@ -58,33 +36,24 @@ async function main() {
|
|
|
58
36
|
const cachedCount = Object.keys(cached.fileIndex).length - modifiedCount - deletedCount;
|
|
59
37
|
|
|
60
38
|
if (newCount === 0 && modifiedCount === 0 && deletedCount === 0) {
|
|
61
|
-
// Nothing changed, use cache as-is
|
|
62
39
|
sessions = cached.sessions;
|
|
63
40
|
fileIndex = cached.fileIndex;
|
|
64
41
|
console.log(`Parsing sessions... ${cached.sessions.length} cached (${Date.now() - startParse}ms)`);
|
|
65
42
|
} else {
|
|
66
|
-
|
|
67
|
-
const { sessions: freshSessions, fileIndex: freshIndex } = await parseAllProjects(claudeDir, days, opts.project);
|
|
68
|
-
|
|
69
|
-
// For a simpler approach: just do a full re-parse when files change
|
|
70
|
-
// This avoids complex merging logic while still benefiting from caching
|
|
71
|
-
// when nothing has changed
|
|
43
|
+
const { sessions: freshSessions, fileIndex: freshIndex } = await parseAllProjects(claudeDir, days, project);
|
|
72
44
|
sessions = freshSessions;
|
|
73
45
|
fileIndex = freshIndex;
|
|
74
46
|
console.log(`Parsing sessions... ${newCount} new, ${modifiedCount} updated, ${Math.max(0, cachedCount)} cached (${Date.now() - startParse}ms)`);
|
|
75
47
|
}
|
|
76
48
|
} else {
|
|
77
|
-
|
|
78
|
-
const result = await parseAllProjects(claudeDir, days, opts.project);
|
|
49
|
+
const result = await parseAllProjects(claudeDir, days, project);
|
|
79
50
|
sessions = result.sessions;
|
|
80
51
|
fileIndex = result.fileIndex;
|
|
81
52
|
console.log(`Parsing sessions... ${sessions.length} parsed (${Date.now() - startParse}ms)`);
|
|
82
53
|
}
|
|
83
54
|
|
|
84
55
|
if (sessions.length === 0) {
|
|
85
|
-
|
|
86
|
-
console.log('Make sure you have used Claude Code and session files exist in ~/.claude/projects/');
|
|
87
|
-
process.exit(0);
|
|
56
|
+
return null;
|
|
88
57
|
}
|
|
89
58
|
|
|
90
59
|
// Step 2: Analyze git repos
|
|
@@ -107,14 +76,48 @@ async function main() {
|
|
|
107
76
|
// Save cache for next run
|
|
108
77
|
saveCache(sessions, fileIndex);
|
|
109
78
|
|
|
110
|
-
|
|
79
|
+
return payload;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function main() {
|
|
83
|
+
const program = new Command();
|
|
84
|
+
program
|
|
85
|
+
.name('claude-roi')
|
|
86
|
+
.description('Correlate Claude Code token usage with git output to measure AI coding agent ROI')
|
|
87
|
+
.version(VERSION)
|
|
88
|
+
.option('-p, --port <number>', 'port to serve dashboard', '3457')
|
|
89
|
+
.option('-d, --days <number>', 'number of days to look back', '30')
|
|
90
|
+
.option('--no-open', 'do not auto-open browser')
|
|
91
|
+
.option('--json', 'output raw JSON to stdout instead of starting server')
|
|
92
|
+
.option('--project <name>', 'filter to specific project')
|
|
93
|
+
.option('--refresh', 'force full re-parse, ignore cache');
|
|
94
|
+
|
|
95
|
+
program.parse();
|
|
96
|
+
const opts = program.opts();
|
|
97
|
+
const port = parseInt(opts.port, 10);
|
|
98
|
+
const days = parseInt(opts.days, 10);
|
|
99
|
+
|
|
100
|
+
console.log(`\x1b[36mclaude-roi\x1b[0m v${VERSION}`);
|
|
101
|
+
|
|
102
|
+
const claudeDir = path.join(os.homedir(), '.claude', 'projects');
|
|
103
|
+
|
|
104
|
+
const payload = await buildPayload(claudeDir, days, opts.project, opts.refresh);
|
|
105
|
+
|
|
106
|
+
if (!payload) {
|
|
107
|
+
console.log('\x1b[33mNo Claude Code sessions found.\x1b[0m');
|
|
108
|
+
console.log('Make sure you have used Claude Code and session files exist in ~/.claude/projects/');
|
|
109
|
+
process.exit(0);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Output
|
|
111
113
|
if (opts.json) {
|
|
112
114
|
process.stdout.write(JSON.stringify(payload, null, 2));
|
|
113
115
|
process.exit(0);
|
|
114
116
|
}
|
|
115
117
|
|
|
116
|
-
// Start server
|
|
117
|
-
const
|
|
118
|
+
// Start server — pass a rebuild function so /api/refresh can re-run the pipeline
|
|
119
|
+
const rebuild = () => buildPayload(claudeDir, days, opts.project, true);
|
|
120
|
+
const app = createServer(payload, rebuild);
|
|
118
121
|
const server = app.listen(port, () => {
|
|
119
122
|
const url = `http://localhost:${port}`;
|
|
120
123
|
console.log(`\x1b[32mDashboard:\x1b[0m ${url}`);
|
package/src/server.js
CHANGED
|
@@ -5,8 +5,9 @@ import path from 'node:path';
|
|
|
5
5
|
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
7
|
|
|
8
|
-
export function createServer(
|
|
8
|
+
export function createServer(initialPayload, rebuildFn) {
|
|
9
9
|
const app = express();
|
|
10
|
+
let payload = initialPayload;
|
|
10
11
|
|
|
11
12
|
// Serve dashboard HTML
|
|
12
13
|
const dashboardHtml = readFileSync(path.join(__dirname, 'dashboard.html'), 'utf-8');
|
|
@@ -20,6 +21,22 @@ export function createServer(payload) {
|
|
|
20
21
|
res.json(payload);
|
|
21
22
|
});
|
|
22
23
|
|
|
24
|
+
// Re-run the full pipeline: clear cache, re-parse sessions, re-analyze git, recompute metrics
|
|
25
|
+
app.post('/api/refresh', async (req, res) => {
|
|
26
|
+
if (!rebuildFn) return res.status(501).json({ error: 'Refresh not available' });
|
|
27
|
+
try {
|
|
28
|
+
console.log('\x1b[36m[refresh]\x1b[0m Re-parsing sessions and recomputing metrics...');
|
|
29
|
+
const newPayload = await rebuildFn();
|
|
30
|
+
if (!newPayload) return res.status(404).json({ error: 'No sessions found after refresh' });
|
|
31
|
+
payload = newPayload;
|
|
32
|
+
console.log('\x1b[32m[refresh]\x1b[0m Done');
|
|
33
|
+
res.json({ ok: true });
|
|
34
|
+
} catch (err) {
|
|
35
|
+
console.error('\x1b[31m[refresh]\x1b[0m Error:', err.message);
|
|
36
|
+
res.status(500).json({ error: err.message });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
23
40
|
// Hero stats + insights
|
|
24
41
|
app.get('/api/summary', (req, res) => {
|
|
25
42
|
res.json({
|