claude-roi 0.1.0
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 +191 -0
- package/package.json +34 -0
- package/src/cache.js +86 -0
- package/src/claude-parser.js +462 -0
- package/src/correlator.js +103 -0
- package/src/dashboard.html +995 -0
- package/src/git-analyzer.js +170 -0
- package/src/index.js +138 -0
- package/src/metrics.js +396 -0
- package/src/server.js +116 -0
|
@@ -0,0 +1,995 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
|
+
<title>Codelens AI — Agent Productivity Dashboard</title>
|
|
7
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
|
8
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
|
|
9
|
+
<style>
|
|
10
|
+
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
|
|
11
|
+
|
|
12
|
+
:root {
|
|
13
|
+
--bg-primary: #0d1117;
|
|
14
|
+
--bg-secondary: #161b22;
|
|
15
|
+
--bg-card: #1c2128;
|
|
16
|
+
--bg-hover: #262c36;
|
|
17
|
+
--border: #30363d;
|
|
18
|
+
--text-primary: #e6edf3;
|
|
19
|
+
--text-secondary: #8b949e;
|
|
20
|
+
--text-muted: #6e7681;
|
|
21
|
+
--accent-green: #3fb950;
|
|
22
|
+
--accent-red: #f85149;
|
|
23
|
+
--accent-orange: #d29922;
|
|
24
|
+
--accent-blue: #58a6ff;
|
|
25
|
+
--accent-purple: #bc8cff;
|
|
26
|
+
--accent-cyan: #39d2c0;
|
|
27
|
+
--grade-a: #3fb950;
|
|
28
|
+
--grade-b: #58a6ff;
|
|
29
|
+
--grade-c: #d29922;
|
|
30
|
+
--grade-d: #f0883e;
|
|
31
|
+
--grade-f: #f85149;
|
|
32
|
+
--radius: 12px;
|
|
33
|
+
--radius-sm: 8px;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
body {
|
|
37
|
+
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
|
38
|
+
background: var(--bg-primary);
|
|
39
|
+
color: var(--text-primary);
|
|
40
|
+
line-height: 1.6;
|
|
41
|
+
min-height: 100vh;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
.container { max-width: 1280px; margin: 0 auto; padding: 24px; }
|
|
45
|
+
|
|
46
|
+
/* Header */
|
|
47
|
+
header {
|
|
48
|
+
text-align: center;
|
|
49
|
+
padding: 40px 0 32px;
|
|
50
|
+
border-bottom: 1px solid var(--border);
|
|
51
|
+
margin-bottom: 32px;
|
|
52
|
+
}
|
|
53
|
+
header h1 {
|
|
54
|
+
font-size: 2.5rem;
|
|
55
|
+
font-weight: 700;
|
|
56
|
+
background: linear-gradient(135deg, var(--accent-blue), var(--accent-purple));
|
|
57
|
+
-webkit-background-clip: text;
|
|
58
|
+
-webkit-text-fill-color: transparent;
|
|
59
|
+
margin-bottom: 8px;
|
|
60
|
+
}
|
|
61
|
+
header .tagline {
|
|
62
|
+
color: var(--text-secondary);
|
|
63
|
+
font-size: 1.1rem;
|
|
64
|
+
font-weight: 300;
|
|
65
|
+
}
|
|
66
|
+
.meta-info {
|
|
67
|
+
display: flex;
|
|
68
|
+
align-items: center;
|
|
69
|
+
justify-content: center;
|
|
70
|
+
gap: 24px;
|
|
71
|
+
margin-top: 16px;
|
|
72
|
+
font-size: 0.85rem;
|
|
73
|
+
color: var(--text-muted);
|
|
74
|
+
}
|
|
75
|
+
.meta-info .badge {
|
|
76
|
+
background: var(--bg-card);
|
|
77
|
+
border: 1px solid var(--border);
|
|
78
|
+
border-radius: 20px;
|
|
79
|
+
padding: 4px 12px;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/* Hero Stats */
|
|
83
|
+
.hero-stats {
|
|
84
|
+
display: grid;
|
|
85
|
+
grid-template-columns: repeat(4, 1fr);
|
|
86
|
+
gap: 16px;
|
|
87
|
+
margin-bottom: 32px;
|
|
88
|
+
}
|
|
89
|
+
.stat-card {
|
|
90
|
+
background: var(--bg-card);
|
|
91
|
+
border: 1px solid var(--border);
|
|
92
|
+
border-radius: var(--radius);
|
|
93
|
+
padding: 24px;
|
|
94
|
+
text-align: center;
|
|
95
|
+
transition: transform 0.2s, border-color 0.2s;
|
|
96
|
+
}
|
|
97
|
+
.stat-card:hover {
|
|
98
|
+
transform: translateY(-2px);
|
|
99
|
+
border-color: var(--accent-blue);
|
|
100
|
+
}
|
|
101
|
+
.stat-card .label {
|
|
102
|
+
font-size: 0.8rem;
|
|
103
|
+
color: var(--text-secondary);
|
|
104
|
+
text-transform: uppercase;
|
|
105
|
+
letter-spacing: 0.5px;
|
|
106
|
+
margin-bottom: 8px;
|
|
107
|
+
}
|
|
108
|
+
.stat-card .value {
|
|
109
|
+
font-size: 2rem;
|
|
110
|
+
font-weight: 700;
|
|
111
|
+
}
|
|
112
|
+
.stat-card .sub {
|
|
113
|
+
font-size: 0.8rem;
|
|
114
|
+
color: var(--text-muted);
|
|
115
|
+
margin-top: 4px;
|
|
116
|
+
}
|
|
117
|
+
.stat-card.grade .value {
|
|
118
|
+
font-size: 3rem;
|
|
119
|
+
line-height: 1;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/* Insights */
|
|
123
|
+
.insights {
|
|
124
|
+
margin-bottom: 32px;
|
|
125
|
+
}
|
|
126
|
+
.insights h2 {
|
|
127
|
+
font-size: 1.1rem;
|
|
128
|
+
margin-bottom: 12px;
|
|
129
|
+
color: var(--text-secondary);
|
|
130
|
+
font-weight: 500;
|
|
131
|
+
}
|
|
132
|
+
.insight-list {
|
|
133
|
+
display: grid;
|
|
134
|
+
gap: 8px;
|
|
135
|
+
}
|
|
136
|
+
.insight {
|
|
137
|
+
display: flex;
|
|
138
|
+
align-items: flex-start;
|
|
139
|
+
gap: 10px;
|
|
140
|
+
padding: 12px 16px;
|
|
141
|
+
background: var(--bg-secondary);
|
|
142
|
+
border: 1px solid var(--border);
|
|
143
|
+
border-radius: var(--radius-sm);
|
|
144
|
+
font-size: 0.9rem;
|
|
145
|
+
}
|
|
146
|
+
.insight .icon { font-size: 1.1rem; flex-shrink: 0; }
|
|
147
|
+
.insight.warning .icon { color: var(--accent-orange); }
|
|
148
|
+
.insight.success .icon { color: var(--accent-green); }
|
|
149
|
+
.insight.info .icon { color: var(--accent-blue); }
|
|
150
|
+
.insight.tip .icon { color: var(--accent-purple); }
|
|
151
|
+
|
|
152
|
+
/* Charts */
|
|
153
|
+
.charts-grid {
|
|
154
|
+
display: grid;
|
|
155
|
+
grid-template-columns: 1fr 1fr;
|
|
156
|
+
gap: 16px;
|
|
157
|
+
margin-bottom: 32px;
|
|
158
|
+
}
|
|
159
|
+
.chart-card {
|
|
160
|
+
background: var(--bg-card);
|
|
161
|
+
border: 1px solid var(--border);
|
|
162
|
+
border-radius: var(--radius);
|
|
163
|
+
padding: 20px;
|
|
164
|
+
}
|
|
165
|
+
.chart-card.full-width {
|
|
166
|
+
grid-column: 1 / -1;
|
|
167
|
+
}
|
|
168
|
+
.chart-card h3 {
|
|
169
|
+
font-size: 0.9rem;
|
|
170
|
+
color: var(--text-secondary);
|
|
171
|
+
margin-bottom: 16px;
|
|
172
|
+
font-weight: 500;
|
|
173
|
+
}
|
|
174
|
+
.chart-header {
|
|
175
|
+
display: flex;
|
|
176
|
+
justify-content: space-between;
|
|
177
|
+
align-items: center;
|
|
178
|
+
margin-bottom: 16px;
|
|
179
|
+
}
|
|
180
|
+
.chart-header h3 { margin-bottom: 0; }
|
|
181
|
+
.scale-toggle {
|
|
182
|
+
padding: 4px 10px;
|
|
183
|
+
border: 1px solid var(--border);
|
|
184
|
+
background: var(--bg-secondary);
|
|
185
|
+
color: var(--text-secondary);
|
|
186
|
+
border-radius: 6px;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
font-family: inherit;
|
|
189
|
+
font-size: 0.75rem;
|
|
190
|
+
}
|
|
191
|
+
.scale-toggle:hover { color: var(--text-primary); border-color: var(--accent-blue); }
|
|
192
|
+
.chart-container {
|
|
193
|
+
position: relative;
|
|
194
|
+
width: 100%;
|
|
195
|
+
height: 300px;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/* Survival bar */
|
|
199
|
+
.survival-section {
|
|
200
|
+
margin-bottom: 32px;
|
|
201
|
+
}
|
|
202
|
+
.survival-card {
|
|
203
|
+
background: var(--bg-card);
|
|
204
|
+
border: 1px solid var(--border);
|
|
205
|
+
border-radius: var(--radius);
|
|
206
|
+
padding: 24px;
|
|
207
|
+
}
|
|
208
|
+
.survival-card h3 {
|
|
209
|
+
font-size: 0.9rem;
|
|
210
|
+
color: var(--text-secondary);
|
|
211
|
+
margin-bottom: 16px;
|
|
212
|
+
font-weight: 500;
|
|
213
|
+
display: flex;
|
|
214
|
+
align-items: center;
|
|
215
|
+
gap: 6px;
|
|
216
|
+
}
|
|
217
|
+
.info-tip {
|
|
218
|
+
display: inline-flex;
|
|
219
|
+
align-items: center;
|
|
220
|
+
justify-content: center;
|
|
221
|
+
width: 16px;
|
|
222
|
+
height: 16px;
|
|
223
|
+
border-radius: 50%;
|
|
224
|
+
background: var(--bg-hover);
|
|
225
|
+
color: var(--text-muted);
|
|
226
|
+
font-size: 0.6rem;
|
|
227
|
+
font-style: normal;
|
|
228
|
+
cursor: help;
|
|
229
|
+
margin-left: 6px;
|
|
230
|
+
position: relative;
|
|
231
|
+
vertical-align: middle;
|
|
232
|
+
flex-shrink: 0;
|
|
233
|
+
}
|
|
234
|
+
.info-tip:hover::after {
|
|
235
|
+
content: attr(data-tip);
|
|
236
|
+
position: absolute;
|
|
237
|
+
bottom: calc(100% + 8px);
|
|
238
|
+
left: 50%;
|
|
239
|
+
transform: translateX(-50%);
|
|
240
|
+
background: var(--bg-primary);
|
|
241
|
+
color: var(--text-secondary);
|
|
242
|
+
padding: 8px 12px;
|
|
243
|
+
border-radius: 6px;
|
|
244
|
+
font-size: 0.75rem;
|
|
245
|
+
font-weight: 400;
|
|
246
|
+
width: 280px;
|
|
247
|
+
line-height: 1.4;
|
|
248
|
+
border: 1px solid var(--border);
|
|
249
|
+
z-index: 100;
|
|
250
|
+
pointer-events: none;
|
|
251
|
+
box-shadow: 0 4px 12px rgba(0,0,0,0.3);
|
|
252
|
+
white-space: normal;
|
|
253
|
+
text-transform: none;
|
|
254
|
+
letter-spacing: normal;
|
|
255
|
+
}
|
|
256
|
+
/* Table header tooltips: show below the icon instead of above */
|
|
257
|
+
thead .info-tip:hover::after {
|
|
258
|
+
bottom: auto;
|
|
259
|
+
top: calc(100% + 8px);
|
|
260
|
+
}
|
|
261
|
+
/* Make table header tooltip icons more visible */
|
|
262
|
+
thead .info-tip {
|
|
263
|
+
background: rgba(255,255,255,0.1);
|
|
264
|
+
color: var(--text-secondary);
|
|
265
|
+
}
|
|
266
|
+
.survival-bar {
|
|
267
|
+
height: 24px;
|
|
268
|
+
background: var(--bg-hover);
|
|
269
|
+
border-radius: 12px;
|
|
270
|
+
overflow: hidden;
|
|
271
|
+
margin-bottom: 12px;
|
|
272
|
+
}
|
|
273
|
+
.survival-bar .fill {
|
|
274
|
+
height: 100%;
|
|
275
|
+
border-radius: 12px;
|
|
276
|
+
transition: width 0.8s ease;
|
|
277
|
+
}
|
|
278
|
+
.survival-stats {
|
|
279
|
+
display: flex;
|
|
280
|
+
gap: 24px;
|
|
281
|
+
font-size: 0.85rem;
|
|
282
|
+
color: var(--text-secondary);
|
|
283
|
+
}
|
|
284
|
+
.survival-stats span { color: var(--text-primary); font-weight: 500; }
|
|
285
|
+
|
|
286
|
+
/* Heatmap */
|
|
287
|
+
.heatmap-grid {
|
|
288
|
+
display: grid;
|
|
289
|
+
grid-template-columns: 40px repeat(24, 1fr);
|
|
290
|
+
gap: 2px;
|
|
291
|
+
}
|
|
292
|
+
.heatmap-label {
|
|
293
|
+
font-size: 0.7rem;
|
|
294
|
+
color: var(--text-muted);
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
justify-content: flex-end;
|
|
298
|
+
padding-right: 6px;
|
|
299
|
+
}
|
|
300
|
+
.heatmap-cell {
|
|
301
|
+
aspect-ratio: 1;
|
|
302
|
+
border-radius: 3px;
|
|
303
|
+
min-height: 14px;
|
|
304
|
+
transition: transform 0.15s;
|
|
305
|
+
cursor: default;
|
|
306
|
+
}
|
|
307
|
+
.heatmap-cell:hover { transform: scale(1.3); }
|
|
308
|
+
.heatmap-hour-labels {
|
|
309
|
+
display: grid;
|
|
310
|
+
grid-template-columns: 40px repeat(24, 1fr);
|
|
311
|
+
gap: 2px;
|
|
312
|
+
margin-top: 4px;
|
|
313
|
+
}
|
|
314
|
+
.heatmap-hour-label {
|
|
315
|
+
font-size: 0.6rem;
|
|
316
|
+
color: var(--text-muted);
|
|
317
|
+
text-align: center;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/* Sessions table */
|
|
321
|
+
.sessions-section {
|
|
322
|
+
margin-bottom: 32px;
|
|
323
|
+
}
|
|
324
|
+
.sessions-section h2 {
|
|
325
|
+
font-size: 1.1rem;
|
|
326
|
+
margin-bottom: 12px;
|
|
327
|
+
color: var(--text-secondary);
|
|
328
|
+
font-weight: 500;
|
|
329
|
+
}
|
|
330
|
+
.sessions-table-wrap {
|
|
331
|
+
background: var(--bg-card);
|
|
332
|
+
border: 1px solid var(--border);
|
|
333
|
+
border-radius: var(--radius);
|
|
334
|
+
overflow-x: auto;
|
|
335
|
+
overflow-y: visible;
|
|
336
|
+
}
|
|
337
|
+
table {
|
|
338
|
+
width: 100%;
|
|
339
|
+
border-collapse: collapse;
|
|
340
|
+
font-size: 0.85rem;
|
|
341
|
+
}
|
|
342
|
+
thead th {
|
|
343
|
+
padding: 12px 12px;
|
|
344
|
+
text-align: left;
|
|
345
|
+
color: var(--text-secondary);
|
|
346
|
+
font-weight: 500;
|
|
347
|
+
border-bottom: 1px solid var(--border);
|
|
348
|
+
cursor: pointer;
|
|
349
|
+
user-select: none;
|
|
350
|
+
white-space: nowrap;
|
|
351
|
+
}
|
|
352
|
+
thead th:hover { color: var(--text-primary); }
|
|
353
|
+
thead th.sorted { color: var(--accent-blue); }
|
|
354
|
+
tbody tr {
|
|
355
|
+
border-bottom: 1px solid var(--border);
|
|
356
|
+
transition: background 0.15s;
|
|
357
|
+
}
|
|
358
|
+
tbody tr:hover { background: var(--bg-hover); }
|
|
359
|
+
tbody tr:last-child { border-bottom: none; }
|
|
360
|
+
tbody td {
|
|
361
|
+
padding: 10px 12px;
|
|
362
|
+
white-space: nowrap;
|
|
363
|
+
}
|
|
364
|
+
.grade-badge {
|
|
365
|
+
display: inline-flex;
|
|
366
|
+
align-items: center;
|
|
367
|
+
justify-content: center;
|
|
368
|
+
width: 28px;
|
|
369
|
+
height: 28px;
|
|
370
|
+
border-radius: 6px;
|
|
371
|
+
font-weight: 700;
|
|
372
|
+
font-size: 0.85rem;
|
|
373
|
+
}
|
|
374
|
+
tr.orphaned { background: rgba(210, 153, 34, 0.08); }
|
|
375
|
+
.expand-row {
|
|
376
|
+
display: none;
|
|
377
|
+
background: var(--bg-secondary);
|
|
378
|
+
}
|
|
379
|
+
.expand-row.open { display: table-row; }
|
|
380
|
+
.expand-row td {
|
|
381
|
+
white-space: normal;
|
|
382
|
+
max-width: 1px;
|
|
383
|
+
}
|
|
384
|
+
.expand-content {
|
|
385
|
+
padding: 12px 16px;
|
|
386
|
+
font-size: 0.8rem;
|
|
387
|
+
}
|
|
388
|
+
.expand-content .commit-item {
|
|
389
|
+
display: flex;
|
|
390
|
+
align-items: center;
|
|
391
|
+
gap: 10px;
|
|
392
|
+
padding: 6px 0;
|
|
393
|
+
border-bottom: 1px solid var(--border);
|
|
394
|
+
overflow: hidden;
|
|
395
|
+
}
|
|
396
|
+
.expand-content .commit-item:last-child { border-bottom: none; }
|
|
397
|
+
.commit-subject {
|
|
398
|
+
overflow: hidden;
|
|
399
|
+
text-overflow: ellipsis;
|
|
400
|
+
white-space: nowrap;
|
|
401
|
+
min-width: 0;
|
|
402
|
+
flex: 1;
|
|
403
|
+
}
|
|
404
|
+
.commit-hash {
|
|
405
|
+
font-family: 'SF Mono', Monaco, monospace;
|
|
406
|
+
color: var(--accent-blue);
|
|
407
|
+
font-size: 0.8rem;
|
|
408
|
+
}
|
|
409
|
+
.commit-branch {
|
|
410
|
+
font-size: 0.7rem;
|
|
411
|
+
padding: 2px 6px;
|
|
412
|
+
border-radius: 4px;
|
|
413
|
+
background: var(--bg-hover);
|
|
414
|
+
color: var(--text-muted);
|
|
415
|
+
}
|
|
416
|
+
.main-badge { background: rgba(63, 185, 80, 0.15); color: var(--accent-green); }
|
|
417
|
+
.pagination {
|
|
418
|
+
display: flex;
|
|
419
|
+
align-items: center;
|
|
420
|
+
justify-content: center;
|
|
421
|
+
gap: 12px;
|
|
422
|
+
padding: 12px;
|
|
423
|
+
border-top: 1px solid var(--border);
|
|
424
|
+
}
|
|
425
|
+
.pagination button {
|
|
426
|
+
padding: 6px 14px;
|
|
427
|
+
border: 1px solid var(--border);
|
|
428
|
+
background: var(--bg-secondary);
|
|
429
|
+
color: var(--text-primary);
|
|
430
|
+
border-radius: 6px;
|
|
431
|
+
cursor: pointer;
|
|
432
|
+
font-family: inherit;
|
|
433
|
+
font-size: 0.8rem;
|
|
434
|
+
}
|
|
435
|
+
.pagination button:disabled { opacity: 0.4; cursor: default; }
|
|
436
|
+
.pagination span { font-size: 0.8rem; color: var(--text-muted); }
|
|
437
|
+
|
|
438
|
+
/* Footer */
|
|
439
|
+
footer {
|
|
440
|
+
text-align: center;
|
|
441
|
+
padding: 32px 0;
|
|
442
|
+
border-top: 1px solid var(--border);
|
|
443
|
+
color: var(--text-muted);
|
|
444
|
+
font-size: 0.8rem;
|
|
445
|
+
}
|
|
446
|
+
footer a { color: var(--accent-blue); text-decoration: none; }
|
|
447
|
+
footer a:hover { text-decoration: underline; }
|
|
448
|
+
|
|
449
|
+
/* Loading */
|
|
450
|
+
.loading {
|
|
451
|
+
display: flex;
|
|
452
|
+
align-items: center;
|
|
453
|
+
justify-content: center;
|
|
454
|
+
padding: 80px 0;
|
|
455
|
+
color: var(--text-secondary);
|
|
456
|
+
}
|
|
457
|
+
.spinner {
|
|
458
|
+
width: 24px;
|
|
459
|
+
height: 24px;
|
|
460
|
+
border: 3px solid var(--border);
|
|
461
|
+
border-top-color: var(--accent-blue);
|
|
462
|
+
border-radius: 50%;
|
|
463
|
+
animation: spin 0.8s linear infinite;
|
|
464
|
+
margin-right: 12px;
|
|
465
|
+
}
|
|
466
|
+
@keyframes spin { to { transform: rotate(360deg); } }
|
|
467
|
+
|
|
468
|
+
/* Responsive */
|
|
469
|
+
@media (max-width: 900px) {
|
|
470
|
+
.hero-stats { grid-template-columns: repeat(2, 1fr); }
|
|
471
|
+
.charts-grid { grid-template-columns: 1fr; }
|
|
472
|
+
}
|
|
473
|
+
@media (max-width: 600px) {
|
|
474
|
+
.hero-stats { grid-template-columns: 1fr; }
|
|
475
|
+
.container { padding: 16px; }
|
|
476
|
+
header h1 { font-size: 1.8rem; }
|
|
477
|
+
}
|
|
478
|
+
</style>
|
|
479
|
+
</head>
|
|
480
|
+
<body>
|
|
481
|
+
<div class="container">
|
|
482
|
+
<header>
|
|
483
|
+
<h1>Codelens AI</h1>
|
|
484
|
+
<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>
|
|
485
|
+
<div class="meta-info">
|
|
486
|
+
<span class="badge" id="date-range"></span>
|
|
487
|
+
<span class="badge">All data stays local</span>
|
|
488
|
+
</div>
|
|
489
|
+
</header>
|
|
490
|
+
|
|
491
|
+
<div id="app">
|
|
492
|
+
<div class="loading">
|
|
493
|
+
<div class="spinner"></div>
|
|
494
|
+
Loading dashboard data...
|
|
495
|
+
</div>
|
|
496
|
+
</div>
|
|
497
|
+
|
|
498
|
+
<footer>
|
|
499
|
+
Made by <a href="https://www.linkedin.com/in/akshat2634/">Akshat</a> · Powered by <a href="https://code.claude.com/docs/en/overview">Claude Code</a> · <a href="https://github.com/Akshat2634/Codelens-AI">GitHub</a>
|
|
500
|
+
<div style="margin-top:6px;font-size:0.75rem;opacity:0.7;">Open source — contributions, ideas, and feedback welcome! <a href="https://github.com/Akshat2634/Codelens-AI" style="color:var(--accent-blue);">Star the repo</a> if you find it useful.</div>
|
|
501
|
+
<div style="margin-top:6px;font-size:0.7rem;opacity:0.4;">Cost estimates are approximate — based on Anthropic's published per-token pricing and may vary from actual billing. Currently supports Claude Code only. Support for Cursor, Codex, Gemini CLI, and more coming soon.</div>
|
|
502
|
+
</footer>
|
|
503
|
+
</div>
|
|
504
|
+
|
|
505
|
+
<script>
|
|
506
|
+
const GRADE_COLORS = { A: '#3fb950', B: '#58a6ff', C: '#d29922', D: '#f0883e', F: '#f85149' };
|
|
507
|
+
const INSIGHT_ICONS = { warning: '!', success: '+', info: 'i', tip: '*' };
|
|
508
|
+
const DAY_LABELS = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
509
|
+
let DATA = null;
|
|
510
|
+
let currentPage = 1;
|
|
511
|
+
let sortCol = 'startTime';
|
|
512
|
+
let sortOrder = -1;
|
|
513
|
+
let timelineChart = null;
|
|
514
|
+
let timelineLogScale = false;
|
|
515
|
+
|
|
516
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
517
|
+
try {
|
|
518
|
+
const res = await fetch('/api/all');
|
|
519
|
+
DATA = await res.json();
|
|
520
|
+
render();
|
|
521
|
+
} catch (err) {
|
|
522
|
+
document.getElementById('app').innerHTML = `
|
|
523
|
+
<div class="loading" style="color: var(--accent-red);">
|
|
524
|
+
Failed to load data: ${err.message}
|
|
525
|
+
</div>`;
|
|
526
|
+
}
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
function render() {
|
|
530
|
+
const d = DATA;
|
|
531
|
+
document.getElementById('date-range').textContent = `Last ${d.meta.daysAnalyzed} days`;
|
|
532
|
+
const summary = d.summary;
|
|
533
|
+
|
|
534
|
+
document.getElementById('app').innerHTML = `
|
|
535
|
+
${renderHeroStats(summary)}
|
|
536
|
+
${renderInsights(d.insights)}
|
|
537
|
+
<div class="charts-grid">
|
|
538
|
+
<div class="chart-card full-width">
|
|
539
|
+
<div class="chart-header">
|
|
540
|
+
<h3>Cost vs Output Over Time <i class="info-tip" data-tip="Orange line = estimated dollar cost per day. Green line = lines of code added per day. Ideally green stays high while orange stays low.">i</i></h3>
|
|
541
|
+
<button class="scale-toggle" onclick="toggleTimelineScale()">Log scale</button>
|
|
542
|
+
</div>
|
|
543
|
+
<div class="chart-container"><canvas id="chart-timeline"></canvas></div>
|
|
544
|
+
</div>
|
|
545
|
+
<div class="chart-card">
|
|
546
|
+
<h3>Model Cost Breakdown <i class="info-tip" data-tip="How your spending splits across different Claude models (Opus, Sonnet, Haiku). Opus is the most expensive but may be more capable.">i</i></h3>
|
|
547
|
+
<div class="chart-container"><canvas id="chart-models"></canvas></div>
|
|
548
|
+
</div>
|
|
549
|
+
<div class="chart-card">
|
|
550
|
+
<h3>Tool Usage Distribution <i class="info-tip" data-tip="Which tools Claude used most during your sessions. Read = reading files, Bash = running commands, Edit = modifying files, Write = creating new files.">i</i></h3>
|
|
551
|
+
<div class="chart-container"><canvas id="chart-tools"></canvas></div>
|
|
552
|
+
</div>
|
|
553
|
+
<div class="chart-card">
|
|
554
|
+
<h3>Session Length vs Efficiency <i class="info-tip" data-tip="Sessions grouped by message count (1-50, 51-100, etc). Blue bars = average cost per commit for that group. Purple bars = number of sessions. Find your sweet spot — the group with the lowest blue bar.">i</i></h3>
|
|
555
|
+
<div class="chart-container"><canvas id="chart-buckets"></canvas></div>
|
|
556
|
+
</div>
|
|
557
|
+
<div class="chart-card">
|
|
558
|
+
<h3>Productivity Heatmap <i class="info-tip" data-tip="Each cell = commits produced during that hour and day of week. Darker green = more commits. Find your peak productivity windows.">i</i></h3>
|
|
559
|
+
<div id="heatmap-container"></div>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
${renderSurvival(d.lineSurvival)}
|
|
563
|
+
${renderSessionsTable(d.sessions)}
|
|
564
|
+
`;
|
|
565
|
+
|
|
566
|
+
initCharts();
|
|
567
|
+
initHeatmap();
|
|
568
|
+
bindEvents();
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
function renderHeroStats(s) {
|
|
572
|
+
const gradeColor = GRADE_COLORS[s.overallGrade] || GRADE_COLORS.F;
|
|
573
|
+
return `<div class="hero-stats">
|
|
574
|
+
<div class="stat-card">
|
|
575
|
+
<div class="label">Total AI Cost <i class="info-tip" data-tip="Estimated cost based on Anthropic's per-token pricing (input/output per 1M tokens): Opus 4.5/4.6: $5/$25, Opus 4.0/4.1: $15/$75, Sonnet 3.7/4.0/4.5/4.6: $3/$15, Haiku 4.5: $1/$5, Haiku 3.5: $0.80/$4, Haiku 3: $0.25/$1.25. Cache reads 90% cheaper, cache writes 25% more.">i</i></div>
|
|
576
|
+
<div class="value" style="color: var(--accent-orange);">$${s.totalCost.toFixed(2)}</div>
|
|
577
|
+
<div class="sub">${s.totalSessions} sessions</div>
|
|
578
|
+
</div>
|
|
579
|
+
<div class="stat-card">
|
|
580
|
+
<div class="label">Commits Shipped <i class="info-tip" data-tip="Git commits matched by file overlap — when a commit touches files that Claude edited during a session. For chat-only sessions, falls back to a 2-hour time window.">i</i></div>
|
|
581
|
+
<div class="value" style="color: var(--accent-green);">${s.totalCommits}</div>
|
|
582
|
+
<div class="sub">${s.totalLinesAdded.toLocaleString()} lines added</div>
|
|
583
|
+
</div>
|
|
584
|
+
<div class="stat-card">
|
|
585
|
+
<div class="label">Avg Cost / Commit <i class="info-tip" data-tip="Total AI cost divided by total commits produced. Lower is better — means your agent is efficient.">i</i></div>
|
|
586
|
+
<div class="value" style="color: var(--accent-blue);">${s.avgCostPerCommit !== null ? '$' + s.avgCostPerCommit.toFixed(2) : 'N/A'}</div>
|
|
587
|
+
<div class="sub">${s.totalFilesChanged} files changed</div>
|
|
588
|
+
</div>
|
|
589
|
+
<div class="stat-card grade">
|
|
590
|
+
<div class="label">ROI Grade <i class="info-tip" data-tip="Overall efficiency grade from A (great) to F (poor). Based on cost-per-commit and code survival. A: ≤$2, B: ≤$5, C: ≤$15, D: ≤$30, F: >$30 or no commits.">i</i></div>
|
|
591
|
+
<div class="value" style="color: ${gradeColor};">${s.overallGrade}</div>
|
|
592
|
+
<div class="sub">${s.orphanedSessionRate}% sessions orphaned</div>
|
|
593
|
+
</div>
|
|
594
|
+
</div>`;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
function renderInsights(insights) {
|
|
598
|
+
if (!insights.length) return '';
|
|
599
|
+
return `<div class="insights">
|
|
600
|
+
<h2>Insights</h2>
|
|
601
|
+
<div class="insight-list">
|
|
602
|
+
${insights.map(i => `
|
|
603
|
+
<div class="insight ${i.type}">
|
|
604
|
+
<span class="icon">${INSIGHT_ICONS[i.type] || 'i'}</span>
|
|
605
|
+
<span>${i.text}</span>
|
|
606
|
+
</div>
|
|
607
|
+
`).join('')}
|
|
608
|
+
</div>
|
|
609
|
+
</div>`;
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
function renderSurvival(s) {
|
|
613
|
+
const pct = s.survivalRate;
|
|
614
|
+
const color = pct >= 80 ? 'var(--accent-green)' : pct >= 50 ? 'var(--accent-orange)' : 'var(--accent-red)';
|
|
615
|
+
return `<div class="survival-section">
|
|
616
|
+
<div class="survival-card">
|
|
617
|
+
<h3>
|
|
618
|
+
Line Survival Rate
|
|
619
|
+
<i class="info-tip" data-tip="Of all lines your AI agent added, how many are still in the codebase after 24 hours. Approximate — based on churn detection, not exact git blame tracking.">i</i>
|
|
620
|
+
</h3>
|
|
621
|
+
<div class="survival-bar"><div class="fill" style="width: ${pct}%; background: ${color};"></div></div>
|
|
622
|
+
<div class="survival-stats">
|
|
623
|
+
<div><span>${s.totalAdded.toLocaleString()}</span> lines added</div>
|
|
624
|
+
<div><span>${s.totalChurned.toLocaleString()}</span> churned within 24h</div>
|
|
625
|
+
<div><span>${s.surviving.toLocaleString()}</span> surviving</div>
|
|
626
|
+
<div><span style="color:${color};">${pct}%</span> survival rate</div>
|
|
627
|
+
</div>
|
|
628
|
+
</div>
|
|
629
|
+
</div>`;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function renderSessionsTable(sessions) {
|
|
633
|
+
const pageSize = 20;
|
|
634
|
+
const totalPages = Math.ceil(sessions.length / pageSize);
|
|
635
|
+
const page = Math.min(currentPage, totalPages || 1);
|
|
636
|
+
const sorted = [...sessions].sort((a, b) => {
|
|
637
|
+
const av = getSortValue(a, sortCol);
|
|
638
|
+
const bv = getSortValue(b, sortCol);
|
|
639
|
+
if (typeof av === 'string') return sortOrder * av.localeCompare(bv);
|
|
640
|
+
return sortOrder * ((av ?? -Infinity) - (bv ?? -Infinity));
|
|
641
|
+
});
|
|
642
|
+
const pageData = sorted.slice((page - 1) * pageSize, page * pageSize);
|
|
643
|
+
|
|
644
|
+
const thArrow = col => col === sortCol ? (sortOrder === 1 ? ' ^' : ' v') : '';
|
|
645
|
+
|
|
646
|
+
return `<div class="sessions-section">
|
|
647
|
+
<h2>Sessions (${sessions.length})</h2>
|
|
648
|
+
<div class="sessions-table-wrap">
|
|
649
|
+
<table>
|
|
650
|
+
<thead>
|
|
651
|
+
<tr>
|
|
652
|
+
<th onclick="sortTable('startTime')" class="${sortCol === 'startTime' ? 'sorted' : ''}">Date${thArrow('startTime')}</th>
|
|
653
|
+
<th onclick="sortTable('projectName')" class="${sortCol === 'projectName' ? 'sorted' : ''}">Project${thArrow('projectName')}</th>
|
|
654
|
+
<th onclick="sortTable('model')" class="${sortCol === 'model' ? 'sorted' : ''}">Model${thArrow('model')} <i class="info-tip" data-tip="Primary model used in the session. Models marked (sub) are subagents spawned for background tasks like code search and exploration.">i</i></th>
|
|
655
|
+
<th onclick="sortTable('msgCount')" class="${sortCol === 'msgCount' ? 'sorted' : ''}">Msgs${thArrow('msgCount')} <i class="info-tip" data-tip="Total messages in the session (your messages + Claude's responses).">i</i></th>
|
|
656
|
+
<th onclick="sortTable('cost.totalCost')" class="${sortCol === 'cost.totalCost' ? 'sorted' : ''}">Cost${thArrow('cost.totalCost')} <i class="info-tip" data-tip="Estimated cost based on token usage and Anthropic's pricing. Calculated from input, output, cache read (90% discount), and cache write (25% premium) tokens.">i</i></th>
|
|
657
|
+
<th onclick="sortTable('commitCount')" class="${sortCol === 'commitCount' ? 'sorted' : ''}">Commits${thArrow('commitCount')} <i class="info-tip" data-tip="Git commits that touch files Claude edited in this session. Falls back to time-window for chat-only sessions.">i</i></th>
|
|
658
|
+
<th onclick="sortTable('linesAdded')" class="${sortCol === 'linesAdded' ? 'sorted' : ''}">Lines${thArrow('linesAdded')} <i class="info-tip" data-tip="Lines added / lines deleted in matched commits.">i</i></th>
|
|
659
|
+
<th>Grade <i class="info-tip" data-tip="Session efficiency: A = great ROI, B = good, C = average, D = poor, F = no commits or very high cost.">i</i></th>
|
|
660
|
+
</tr>
|
|
661
|
+
</thead>
|
|
662
|
+
<tbody>
|
|
663
|
+
${pageData.map((s, i) => {
|
|
664
|
+
const idx = (page - 1) * pageSize + i;
|
|
665
|
+
const gradeColor = GRADE_COLORS[s.grade] || GRADE_COLORS.F;
|
|
666
|
+
const primaryName = formatModelName(s.model || 'unknown');
|
|
667
|
+
const subModels = Object.keys(s.modelBreakdown || {})
|
|
668
|
+
.filter(m => m !== s.model)
|
|
669
|
+
.map(formatModelName)
|
|
670
|
+
.filter(Boolean);
|
|
671
|
+
const modelDisplay = subModels.length > 0
|
|
672
|
+
? primaryName + ' <span style="color:var(--text-muted);font-size:0.75rem;">+ ' + subModels.join(', ') + ' <span style="opacity:0.6;">(sub)</span></span>'
|
|
673
|
+
: primaryName;
|
|
674
|
+
const rowClass = s.isOrphaned ? 'orphaned' : '';
|
|
675
|
+
return `
|
|
676
|
+
<tr class="${rowClass}" style="cursor:pointer;" onclick="toggleExpand(${idx})">
|
|
677
|
+
<td>${formatDate(s.startTime)}</td>
|
|
678
|
+
<td>${s.projectName || '—'}</td>
|
|
679
|
+
<td>${modelDisplay}</td>
|
|
680
|
+
<td>${s.userMessageCount + s.assistantMessageCount}</td>
|
|
681
|
+
<td>$${s.cost.totalCost.toFixed(2)}</td>
|
|
682
|
+
<td>${s.commitCount}</td>
|
|
683
|
+
<td><span style="color:#3fb950">+${s.linesAdded.toLocaleString()}</span> / <span style="color:#f85149">-${s.linesDeleted.toLocaleString()}</span></td>
|
|
684
|
+
<td><span class="grade-badge" style="background:${gradeColor}22;color:${gradeColor};">${s.grade}</span></td>
|
|
685
|
+
</tr>
|
|
686
|
+
<tr class="expand-row" id="expand-${idx}">
|
|
687
|
+
<td colspan="8">
|
|
688
|
+
<div class="expand-content">
|
|
689
|
+
${s.commits.length > 0 ? s.commits.map(c => `
|
|
690
|
+
<div class="commit-item">
|
|
691
|
+
<span class="commit-hash">${c.hash.slice(0, 7)}</span>
|
|
692
|
+
<span class="commit-branch ${c.onMain ? 'main-badge' : ''}">${c.onMain ? 'main' : c.branches[0] || 'branch'}</span>
|
|
693
|
+
<span class="commit-subject">${c.subject}</span>
|
|
694
|
+
<span style="margin-left:auto;color:var(--text-muted);white-space:nowrap;flex-shrink:0;">+${c.totalAdded}/-${c.totalDeleted}</span>
|
|
695
|
+
</div>
|
|
696
|
+
`).join('') : '<div style="color:var(--text-muted);">No matched commits</div>'}
|
|
697
|
+
</div>
|
|
698
|
+
</td>
|
|
699
|
+
</tr>
|
|
700
|
+
`;
|
|
701
|
+
}).join('')}
|
|
702
|
+
</tbody>
|
|
703
|
+
</table>
|
|
704
|
+
<div class="pagination">
|
|
705
|
+
<button onclick="changePage(-1)" ${page <= 1 ? 'disabled' : ''}>Prev</button>
|
|
706
|
+
<span>Page ${page} of ${totalPages || 1}</span>
|
|
707
|
+
<button onclick="changePage(1)" ${page >= totalPages ? 'disabled' : ''}>Next</button>
|
|
708
|
+
</div>
|
|
709
|
+
</div>
|
|
710
|
+
</div>`;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
function getSortValue(session, col) {
|
|
714
|
+
if (col === 'cost.totalCost') return session.cost?.totalCost ?? 0;
|
|
715
|
+
if (col === 'msgCount') return (session.userMessageCount || 0) + (session.assistantMessageCount || 0);
|
|
716
|
+
return session[col];
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
function formatModelName(modelId) {
|
|
720
|
+
if (!modelId) return 'Unknown';
|
|
721
|
+
// claude-opus-4-6 → Opus 4.6, claude-sonnet-4-5-20250929 → Sonnet 4.5, claude-haiku-4-5-20251001 → Haiku 4.5
|
|
722
|
+
const m = modelId.toLowerCase().replace('claude-', '');
|
|
723
|
+
const match = m.match(/^(opus|sonnet|haiku)-(\d+)-(\d+)/);
|
|
724
|
+
if (match) return match[1].charAt(0).toUpperCase() + match[1].slice(1) + ' ' + match[2] + '.' + match[3];
|
|
725
|
+
// fallback: capitalize first word
|
|
726
|
+
const family = m.replace(/-\d.*/, '');
|
|
727
|
+
return family.charAt(0).toUpperCase() + family.slice(1);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function formatDate(iso) {
|
|
731
|
+
if (!iso) return '—';
|
|
732
|
+
const d = new Date(iso);
|
|
733
|
+
return d.toLocaleString(undefined, { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', hour12: true });
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function initCharts() {
|
|
737
|
+
Chart.defaults.color = '#8b949e';
|
|
738
|
+
Chart.defaults.borderColor = '#30363d';
|
|
739
|
+
Chart.defaults.font.family = 'Inter';
|
|
740
|
+
|
|
741
|
+
// Timeline chart
|
|
742
|
+
const daily = DATA.daily;
|
|
743
|
+
const avgCost = daily.reduce((s, d) => s + d.cost, 0) / (daily.length || 1);
|
|
744
|
+
timelineChart = new Chart(document.getElementById('chart-timeline'), {
|
|
745
|
+
type: 'line',
|
|
746
|
+
data: {
|
|
747
|
+
labels: daily.map(d => d.date),
|
|
748
|
+
datasets: [
|
|
749
|
+
{
|
|
750
|
+
label: 'Cost ($)',
|
|
751
|
+
data: daily.map(d => d.cost),
|
|
752
|
+
borderColor: '#d29922',
|
|
753
|
+
backgroundColor: 'rgba(210,153,34,0.1)',
|
|
754
|
+
fill: true,
|
|
755
|
+
tension: 0.3,
|
|
756
|
+
yAxisID: 'y',
|
|
757
|
+
pointRadius: daily.map(d => d.cost > avgCost * 3 ? 6 : 3),
|
|
758
|
+
},
|
|
759
|
+
{
|
|
760
|
+
label: 'Lines Added',
|
|
761
|
+
data: daily.map(d => d.linesAdded),
|
|
762
|
+
borderColor: '#3fb950',
|
|
763
|
+
backgroundColor: 'rgba(63,185,80,0.1)',
|
|
764
|
+
fill: true,
|
|
765
|
+
tension: 0.3,
|
|
766
|
+
yAxisID: 'y1',
|
|
767
|
+
},
|
|
768
|
+
],
|
|
769
|
+
},
|
|
770
|
+
options: {
|
|
771
|
+
responsive: true,
|
|
772
|
+
maintainAspectRatio: false,
|
|
773
|
+
interaction: { mode: 'index', intersect: false },
|
|
774
|
+
plugins: {
|
|
775
|
+
legend: { position: 'top' },
|
|
776
|
+
tooltip: {
|
|
777
|
+
callbacks: {
|
|
778
|
+
title: ctx => ctx[0].label,
|
|
779
|
+
afterBody: ctx => {
|
|
780
|
+
const idx = ctx[0].dataIndex;
|
|
781
|
+
const day = DATA.daily[idx];
|
|
782
|
+
return 'Sessions: ' + (day.sessions || 0);
|
|
783
|
+
},
|
|
784
|
+
},
|
|
785
|
+
},
|
|
786
|
+
},
|
|
787
|
+
scales: {
|
|
788
|
+
y: {
|
|
789
|
+
type: 'linear', position: 'left',
|
|
790
|
+
title: { display: true, text: 'Cost ($)' },
|
|
791
|
+
ticks: {
|
|
792
|
+
callback: function(v) {
|
|
793
|
+
if (this.type === 'logarithmic') {
|
|
794
|
+
const nice = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000];
|
|
795
|
+
return nice.includes(v) ? '$' + v : '';
|
|
796
|
+
}
|
|
797
|
+
return '$' + v;
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
afterBuildTicks: function(axis) {
|
|
801
|
+
if (axis.type === 'logarithmic') {
|
|
802
|
+
axis.min = 0.5;
|
|
803
|
+
}
|
|
804
|
+
},
|
|
805
|
+
},
|
|
806
|
+
y1: {
|
|
807
|
+
type: 'linear', position: 'right',
|
|
808
|
+
title: { display: true, text: 'Lines Added' },
|
|
809
|
+
grid: { drawOnChartArea: false },
|
|
810
|
+
ticks: {
|
|
811
|
+
callback: function(v) {
|
|
812
|
+
if (this.type === 'logarithmic') {
|
|
813
|
+
const nice = [1, 5, 10, 50, 100, 500, 1000, 5000];
|
|
814
|
+
return nice.includes(v) ? v.toLocaleString() : '';
|
|
815
|
+
}
|
|
816
|
+
return v.toLocaleString();
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
afterBuildTicks: function(axis) {
|
|
820
|
+
if (axis.type === 'logarithmic') {
|
|
821
|
+
axis.min = 1;
|
|
822
|
+
}
|
|
823
|
+
},
|
|
824
|
+
},
|
|
825
|
+
},
|
|
826
|
+
},
|
|
827
|
+
});
|
|
828
|
+
|
|
829
|
+
// Model doughnut
|
|
830
|
+
const models = DATA.modelBreakdown;
|
|
831
|
+
const modelLabels = Object.keys(models).filter(k => models[k].cost > 0);
|
|
832
|
+
const modelColors = { opus: '#bc8cff', sonnet: '#58a6ff', haiku: '#3fb950', unknown: '#8b949e' };
|
|
833
|
+
const modelTotalCost = modelLabels.reduce((s, m) => s + models[m].cost, 0);
|
|
834
|
+
new Chart(document.getElementById('chart-models'), {
|
|
835
|
+
type: 'doughnut',
|
|
836
|
+
data: {
|
|
837
|
+
labels: modelLabels.map(m => {
|
|
838
|
+
const pct = modelTotalCost > 0 ? Math.round((models[m].cost / modelTotalCost) * 100) : 0;
|
|
839
|
+
return m.charAt(0).toUpperCase() + m.slice(1) + ' (' + pct + '%)';
|
|
840
|
+
}),
|
|
841
|
+
datasets: [{
|
|
842
|
+
data: modelLabels.map(m => models[m].cost),
|
|
843
|
+
backgroundColor: modelLabels.map(m => modelColors[m] || '#8b949e'),
|
|
844
|
+
borderWidth: 0,
|
|
845
|
+
}],
|
|
846
|
+
},
|
|
847
|
+
options: {
|
|
848
|
+
responsive: true,
|
|
849
|
+
maintainAspectRatio: false,
|
|
850
|
+
plugins: {
|
|
851
|
+
legend: { position: 'bottom' },
|
|
852
|
+
tooltip: {
|
|
853
|
+
callbacks: {
|
|
854
|
+
label: ctx => {
|
|
855
|
+
const model = modelLabels[ctx.dataIndex];
|
|
856
|
+
const d = models[model];
|
|
857
|
+
return ` $${d.cost.toFixed(2)} | ${d.sessions} sessions | ${d.commits} commits`;
|
|
858
|
+
},
|
|
859
|
+
},
|
|
860
|
+
},
|
|
861
|
+
},
|
|
862
|
+
},
|
|
863
|
+
});
|
|
864
|
+
|
|
865
|
+
// Tool usage horizontal bar
|
|
866
|
+
const tools = DATA.toolBreakdown;
|
|
867
|
+
const toolEntries = Object.entries(tools).sort((a, b) => b[1] - a[1]).slice(0, 10);
|
|
868
|
+
new Chart(document.getElementById('chart-tools'), {
|
|
869
|
+
type: 'bar',
|
|
870
|
+
data: {
|
|
871
|
+
labels: toolEntries.map(([t]) => t),
|
|
872
|
+
datasets: [{
|
|
873
|
+
data: toolEntries.map(([, c]) => c),
|
|
874
|
+
backgroundColor: toolEntries.map((_, i) => {
|
|
875
|
+
const hue = 200 + i * 15;
|
|
876
|
+
return `hsl(${hue}, 60%, 55%)`;
|
|
877
|
+
}),
|
|
878
|
+
borderWidth: 0,
|
|
879
|
+
borderRadius: 4,
|
|
880
|
+
}],
|
|
881
|
+
},
|
|
882
|
+
options: {
|
|
883
|
+
indexAxis: 'y',
|
|
884
|
+
responsive: true,
|
|
885
|
+
maintainAspectRatio: false,
|
|
886
|
+
plugins: { legend: { display: false } },
|
|
887
|
+
scales: { x: { grid: { display: false } } },
|
|
888
|
+
},
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
// Session buckets
|
|
892
|
+
const buckets = DATA.sessionBuckets;
|
|
893
|
+
const bucketLabels = Object.keys(buckets);
|
|
894
|
+
new Chart(document.getElementById('chart-buckets'), {
|
|
895
|
+
type: 'bar',
|
|
896
|
+
data: {
|
|
897
|
+
labels: bucketLabels,
|
|
898
|
+
datasets: [
|
|
899
|
+
{
|
|
900
|
+
label: 'Avg $/Commit',
|
|
901
|
+
data: bucketLabels.map(b => buckets[b].avgCostPerCommit),
|
|
902
|
+
backgroundColor: '#58a6ff',
|
|
903
|
+
borderRadius: 4,
|
|
904
|
+
yAxisID: 'y',
|
|
905
|
+
},
|
|
906
|
+
{
|
|
907
|
+
label: 'Sessions',
|
|
908
|
+
data: bucketLabels.map(b => buckets[b].sessions),
|
|
909
|
+
backgroundColor: 'rgba(188,140,255,0.4)',
|
|
910
|
+
borderRadius: 4,
|
|
911
|
+
yAxisID: 'y1',
|
|
912
|
+
},
|
|
913
|
+
],
|
|
914
|
+
},
|
|
915
|
+
options: {
|
|
916
|
+
responsive: true,
|
|
917
|
+
maintainAspectRatio: false,
|
|
918
|
+
plugins: { legend: { position: 'top' } },
|
|
919
|
+
scales: {
|
|
920
|
+
y: { type: 'linear', position: 'left', title: { display: true, text: 'Avg $/Commit' } },
|
|
921
|
+
y1: { type: 'linear', position: 'right', title: { display: true, text: 'Sessions' }, grid: { drawOnChartArea: false } },
|
|
922
|
+
},
|
|
923
|
+
},
|
|
924
|
+
});
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
function initHeatmap() {
|
|
928
|
+
const container = document.getElementById('heatmap-container');
|
|
929
|
+
if (!container) return;
|
|
930
|
+
const heatmap = DATA.heatmap.commits;
|
|
931
|
+
const maxVal = Math.max(1, ...heatmap.flat());
|
|
932
|
+
|
|
933
|
+
let html = '<div class="heatmap-grid">';
|
|
934
|
+
for (let day = 0; day < 7; day++) {
|
|
935
|
+
html += `<div class="heatmap-label">${DAY_LABELS[day]}</div>`;
|
|
936
|
+
for (let hour = 0; hour < 24; hour++) {
|
|
937
|
+
const val = heatmap[day][hour];
|
|
938
|
+
const intensity = val / maxVal;
|
|
939
|
+
const bg = val === 0 ? 'var(--bg-hover)' : `rgba(63,185,80,${0.2 + intensity * 0.8})`;
|
|
940
|
+
html += `<div class="heatmap-cell" style="background:${bg};" title="${DAY_LABELS[day]} ${hour}:00 — ${val} commits"></div>`;
|
|
941
|
+
}
|
|
942
|
+
}
|
|
943
|
+
html += '</div>';
|
|
944
|
+
|
|
945
|
+
html += '<div class="heatmap-hour-labels"><div></div>';
|
|
946
|
+
for (let h = 0; h < 24; h++) {
|
|
947
|
+
html += `<div class="heatmap-hour-label">${h % 3 === 0 ? h : ''}</div>`;
|
|
948
|
+
}
|
|
949
|
+
html += '</div>';
|
|
950
|
+
|
|
951
|
+
// Gradient legend
|
|
952
|
+
html += '<div style="display:flex;align-items:center;justify-content:flex-end;gap:8px;margin-top:8px;font-size:0.7rem;color:var(--text-muted);">';
|
|
953
|
+
html += '<span>Fewer</span><div style="display:flex;gap:1px;">';
|
|
954
|
+
const legendSteps = [0.2, 0.4, 0.6, 0.8, 1.0];
|
|
955
|
+
for (const step of legendSteps) {
|
|
956
|
+
html += `<div style="width:14px;height:14px;border-radius:3px;background:rgba(63,185,80,${0.2 + step * 0.8});"></div>`;
|
|
957
|
+
}
|
|
958
|
+
html += '</div><span>More commits</span></div>';
|
|
959
|
+
|
|
960
|
+
container.innerHTML = html;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function bindEvents() {}
|
|
964
|
+
|
|
965
|
+
window.toggleExpand = function(idx) {
|
|
966
|
+
const row = document.getElementById(`expand-${idx}`);
|
|
967
|
+
if (row) row.classList.toggle('open');
|
|
968
|
+
};
|
|
969
|
+
|
|
970
|
+
window.sortTable = function(col) {
|
|
971
|
+
if (sortCol === col) { sortOrder *= -1; }
|
|
972
|
+
else { sortCol = col; sortOrder = -1; }
|
|
973
|
+
currentPage = 1;
|
|
974
|
+
render();
|
|
975
|
+
};
|
|
976
|
+
|
|
977
|
+
window.changePage = function(delta) {
|
|
978
|
+
currentPage += delta;
|
|
979
|
+
render();
|
|
980
|
+
};
|
|
981
|
+
|
|
982
|
+
window.toggleTimelineScale = function() {
|
|
983
|
+
if (!timelineChart) return;
|
|
984
|
+
timelineLogScale = !timelineLogScale;
|
|
985
|
+
const scaleType = timelineLogScale ? 'logarithmic' : 'linear';
|
|
986
|
+
timelineChart.options.scales.y.type = scaleType;
|
|
987
|
+
timelineChart.options.scales.y1.type = scaleType;
|
|
988
|
+
timelineChart.update();
|
|
989
|
+
const btn = document.querySelector('.scale-toggle');
|
|
990
|
+
if (btn) btn.textContent = timelineLogScale ? 'Linear scale' : 'Log scale';
|
|
991
|
+
};
|
|
992
|
+
|
|
993
|
+
</script>
|
|
994
|
+
</body>
|
|
995
|
+
</html>
|