cchubber 0.2.0 → 0.3.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/package.json +1 -1
- package/src/analyzers/cache-health.js +11 -4
- package/src/analyzers/inflection-detector.js +38 -26
- package/src/analyzers/recommendations.js +97 -76
- package/src/readers/claude-md.js +2 -3
- package/src/readers/jsonl-reader.js +28 -4
- package/src/renderers/html-report.js +1045 -767
|
@@ -1,767 +1,1045 @@
|
|
|
1
|
-
export function renderHTML(report) {
|
|
2
|
-
const { costAnalysis, cacheHealth, anomalies, inflection, sessionIntel, modelRouting, projectBreakdown, claudeMdStack, oauthUsage, recommendations, generatedAt } = report;
|
|
3
|
-
|
|
4
|
-
const dailyCosts = costAnalysis.dailyCosts || [];
|
|
5
|
-
const grade = cacheHealth.grade || { letter: '?', color: '#666', label: 'Unknown' };
|
|
6
|
-
const totalCost = costAnalysis.totalCost || 0;
|
|
7
|
-
const activeDays = costAnalysis.activeDays || 0;
|
|
8
|
-
const peakDay = costAnalysis.peakDay;
|
|
9
|
-
|
|
10
|
-
const modelCosts = costAnalysis.modelCosts || {};
|
|
11
|
-
const modelEntries = Object.entries(modelCosts).filter(([, c]) => c > 0.01).sort((a, b) => b[1] - a[1]);
|
|
12
|
-
const anomalyDates = new Set((anomalies.anomalies || []).map(a => a.date));
|
|
13
|
-
|
|
14
|
-
const dailyCostsJSON = JSON.stringify(dailyCosts.map(d => ({
|
|
15
|
-
date: d.date, cost: d.cost, cacheOutputRatio: d.cacheOutputRatio || 0, isAnomaly: anomalyDates.has(d.date),
|
|
16
|
-
})));
|
|
17
|
-
|
|
18
|
-
const projectsJSON = JSON.stringify((projectBreakdown || []).slice(0, 15).map(p => ({
|
|
19
|
-
name: p.name, path: p.path, messages: p.messageCount, sessions: p.sessionCount,
|
|
20
|
-
input: p.inputTokens, output: p.outputTokens, cacheRead: p.cacheReadTokens, cacheWrite: p.cacheCreationTokens,
|
|
21
|
-
})));
|
|
22
|
-
|
|
23
|
-
const fmtCost = (n) => '$' + (n >= 100 ? Math.round(n).toLocaleString() : n.toFixed(2));
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
'
|
|
29
|
-
'
|
|
30
|
-
'
|
|
31
|
-
'
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
41
|
-
'
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
: grade.letter === '
|
|
52
|
-
:
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
<
|
|
68
|
-
<
|
|
69
|
-
<meta
|
|
70
|
-
<
|
|
71
|
-
<
|
|
72
|
-
<
|
|
73
|
-
<link href="https://fonts.googleapis.com/css2?family=
|
|
74
|
-
<
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
"
|
|
82
|
-
"surface
|
|
83
|
-
"surface-
|
|
84
|
-
"surface-container-
|
|
85
|
-
"surface-container": "#
|
|
86
|
-
"surface-container
|
|
87
|
-
"surface-container-
|
|
88
|
-
"surface-
|
|
89
|
-
"
|
|
90
|
-
"on-surface
|
|
91
|
-
"on-
|
|
92
|
-
"
|
|
93
|
-
"outline
|
|
94
|
-
"
|
|
95
|
-
"primary
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
"secondary
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
"tertiary
|
|
102
|
-
"
|
|
103
|
-
"error
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"inverse-
|
|
107
|
-
"inverse-
|
|
108
|
-
"
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
"
|
|
119
|
-
"
|
|
120
|
-
"
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
color: #
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
border:
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
.tt
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
<
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
<
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
<div class="
|
|
243
|
-
<
|
|
244
|
-
<
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
</
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
<
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
</
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
</
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
<div class="
|
|
303
|
-
<
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
</
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
</
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
<
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
<div class="
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
<
|
|
378
|
-
<div class="
|
|
379
|
-
<
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
<
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
<div class="
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
<
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
</div>` : ''}
|
|
433
|
-
</div>
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
<
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
<
|
|
468
|
-
<
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
</
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
<
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
<
|
|
505
|
-
</
|
|
506
|
-
</div>
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
<th class="px-8 py-
|
|
522
|
-
<th class="px-8 py-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
</
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
<
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
<
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
</
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
</
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
</
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
1
|
+
export function renderHTML(report) {
|
|
2
|
+
const { costAnalysis, cacheHealth, anomalies, inflection, sessionIntel, modelRouting, projectBreakdown, claudeMdStack, oauthUsage, recommendations, generatedAt } = report;
|
|
3
|
+
|
|
4
|
+
const dailyCosts = costAnalysis.dailyCosts || [];
|
|
5
|
+
const grade = cacheHealth.grade || { letter: '?', color: '#666', label: 'Unknown' };
|
|
6
|
+
const totalCost = costAnalysis.totalCost || 0;
|
|
7
|
+
const activeDays = costAnalysis.activeDays || 0;
|
|
8
|
+
const peakDay = costAnalysis.peakDay;
|
|
9
|
+
|
|
10
|
+
const modelCosts = costAnalysis.modelCosts || {};
|
|
11
|
+
const modelEntries = Object.entries(modelCosts).filter(([, c]) => c > 0.01).sort((a, b) => b[1] - a[1]);
|
|
12
|
+
const anomalyDates = new Set((anomalies.anomalies || []).map(a => a.date));
|
|
13
|
+
|
|
14
|
+
const dailyCostsJSON = JSON.stringify(dailyCosts.map(d => ({
|
|
15
|
+
date: d.date, cost: d.cost, cacheOutputRatio: d.cacheOutputRatio || 0, isAnomaly: anomalyDates.has(d.date),
|
|
16
|
+
})));
|
|
17
|
+
|
|
18
|
+
const projectsJSON = JSON.stringify((projectBreakdown || []).slice(0, 15).map(p => ({
|
|
19
|
+
name: p.name, path: p.path, messages: p.messageCount, sessions: p.sessionCount,
|
|
20
|
+
input: p.inputTokens, output: p.outputTokens, cacheRead: p.cacheReadTokens, cacheWrite: p.cacheCreationTokens,
|
|
21
|
+
})));
|
|
22
|
+
|
|
23
|
+
const fmtCost = (n) => '$' + (n >= 100 ? Math.round(n).toLocaleString() : n.toFixed(2));
|
|
24
|
+
const fmtDuration = (m) => m >= 120 ? Math.round(m/60) + 'h' : m >= 60 ? (m/60).toFixed(1) + 'h' : m + 'm';
|
|
25
|
+
|
|
26
|
+
// Grade color mapping to Stitch palette
|
|
27
|
+
const gradeColorMap = {
|
|
28
|
+
'A': '#c0c1ff', // primary indigo
|
|
29
|
+
'B': '#d4bbff', // tertiary purple
|
|
30
|
+
'C': '#ffb690', // secondary orange
|
|
31
|
+
'D': '#ffb4ab', // error red
|
|
32
|
+
'F': '#ffb4ab', // error red
|
|
33
|
+
};
|
|
34
|
+
const gradeColor = gradeColorMap[grade.letter] || '#908fa0';
|
|
35
|
+
|
|
36
|
+
// Grade label mapping
|
|
37
|
+
const gradeLabelMap = {
|
|
38
|
+
'A': 'Excellent Performance',
|
|
39
|
+
'B': 'Good Performance',
|
|
40
|
+
'C': 'Fair Performance',
|
|
41
|
+
'D': 'Poor Performance',
|
|
42
|
+
'F': 'Critical Performance',
|
|
43
|
+
};
|
|
44
|
+
const gradeLabel = gradeLabelMap[grade.letter] || grade.label || 'Unknown';
|
|
45
|
+
|
|
46
|
+
// Diagnosis one-liner for the share card
|
|
47
|
+
const diagnosisLine = inflection && inflection.direction === 'worsened' && inflection.multiplier >= 2
|
|
48
|
+
? `Efficiency dropped ${inflection.multiplier}x on ${inflection.date}`
|
|
49
|
+
: anomalies.hasAnomalies
|
|
50
|
+
? `${anomalies.anomalies.length} anomal${anomalies.anomalies.length === 1 ? 'y' : 'ies'} detected`
|
|
51
|
+
: grade.letter === 'A' ? 'System running clean'
|
|
52
|
+
: grade.letter === 'B' ? 'Minor optimization opportunities'
|
|
53
|
+
: `Cache efficiency needs attention`;
|
|
54
|
+
|
|
55
|
+
// Model colors from Stitch palette
|
|
56
|
+
const modelColors = ['#c0c1ff', '#d4bbff', '#ffb690', '#8083ff', '#a775ff', '#ffb4ab'];
|
|
57
|
+
|
|
58
|
+
// Severity to Stitch color mapping
|
|
59
|
+
const sevColorMap = {
|
|
60
|
+
critical: { border: '#ffb4ab', bg: 'rgba(255, 180, 171, 0.10)', text: '#ffb4ab' },
|
|
61
|
+
warning: { border: '#ffb690', bg: 'rgba(255, 182, 144, 0.10)', text: '#ffb690' },
|
|
62
|
+
info: { border: '#c0c1ff', bg: 'rgba(192, 193, 255, 0.10)', text: '#c0c1ff' },
|
|
63
|
+
positive: { border: '#c0c1ff', bg: 'rgba(192, 193, 255, 0.10)', text: '#c0c1ff' },
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
return `<!DOCTYPE html>
|
|
67
|
+
<html class="dark" lang="en">
|
|
68
|
+
<head>
|
|
69
|
+
<meta charset="UTF-8">
|
|
70
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
71
|
+
<title>CC Hubber</title>
|
|
72
|
+
<script src="https://cdn.tailwindcss.com?plugins=forms,container-queries"></script>
|
|
73
|
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800;900&family=JetBrains+Mono:wght@400;500;700&display=swap" rel="stylesheet">
|
|
74
|
+
<link href="https://fonts.googleapis.com/css2?family=Material+Symbols+Outlined:wght,FILL@100..700,0..1&display=swap" rel="stylesheet">
|
|
75
|
+
<script>
|
|
76
|
+
tailwind.config = {
|
|
77
|
+
darkMode: "class",
|
|
78
|
+
theme: {
|
|
79
|
+
extend: {
|
|
80
|
+
colors: {
|
|
81
|
+
"background": "#121315",
|
|
82
|
+
"surface": "#121315",
|
|
83
|
+
"surface-dim": "#121315",
|
|
84
|
+
"surface-container-lowest": "#0d0e0f",
|
|
85
|
+
"surface-container-low": "#1b1c1d",
|
|
86
|
+
"surface-container": "#1f2021",
|
|
87
|
+
"surface-container-high": "#292a2b",
|
|
88
|
+
"surface-container-highest": "#343536",
|
|
89
|
+
"surface-bright": "#38393a",
|
|
90
|
+
"on-surface": "#e3e2e3",
|
|
91
|
+
"on-surface-variant": "#c7c4d7",
|
|
92
|
+
"on-background": "#e3e2e3",
|
|
93
|
+
"outline": "#908fa0",
|
|
94
|
+
"outline-variant": "#464554",
|
|
95
|
+
"primary": "#c0c1ff",
|
|
96
|
+
"primary-container": "#8083ff",
|
|
97
|
+
"on-primary": "#1000a9",
|
|
98
|
+
"secondary": "#ffb690",
|
|
99
|
+
"secondary-container": "#ec6a06",
|
|
100
|
+
"on-secondary": "#552100",
|
|
101
|
+
"tertiary": "#d4bbff",
|
|
102
|
+
"tertiary-container": "#a775ff",
|
|
103
|
+
"error": "#ffb4ab",
|
|
104
|
+
"error-container": "#93000a",
|
|
105
|
+
"on-error": "#690005",
|
|
106
|
+
"inverse-surface": "#e3e2e3",
|
|
107
|
+
"inverse-on-surface": "#303032",
|
|
108
|
+
"inverse-primary": "#494bd6",
|
|
109
|
+
"surface-tint": "#c0c1ff",
|
|
110
|
+
},
|
|
111
|
+
fontFamily: {
|
|
112
|
+
"headline": ["Inter", "system-ui", "sans-serif"],
|
|
113
|
+
"body": ["Inter", "system-ui", "sans-serif"],
|
|
114
|
+
"label": ["Inter", "system-ui", "sans-serif"],
|
|
115
|
+
"mono": ["JetBrains Mono", "monospace"],
|
|
116
|
+
},
|
|
117
|
+
borderRadius: {
|
|
118
|
+
"DEFAULT": "0.125rem",
|
|
119
|
+
"lg": "0.25rem",
|
|
120
|
+
"xl": "0.5rem",
|
|
121
|
+
"full": "0.75rem",
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
},
|
|
125
|
+
}
|
|
126
|
+
</script>
|
|
127
|
+
<style>
|
|
128
|
+
body {
|
|
129
|
+
background-color: #121315;
|
|
130
|
+
color: #e3e2e3;
|
|
131
|
+
font-family: 'Inter', system-ui, sans-serif;
|
|
132
|
+
-webkit-font-smoothing: antialiased;
|
|
133
|
+
-moz-osx-font-smoothing: grayscale;
|
|
134
|
+
}
|
|
135
|
+
.font-mono { font-family: 'JetBrains Mono', monospace !important; }
|
|
136
|
+
|
|
137
|
+
/* Info tooltips */
|
|
138
|
+
.has-tip{position:relative;cursor:help;}
|
|
139
|
+
.has-tip .tip{
|
|
140
|
+
position:absolute;top:calc(100% + 8px);left:0;transform:none;
|
|
141
|
+
background:#292a2b;border:1px solid rgba(70,69,84,0.3);border-radius:8px;
|
|
142
|
+
padding:10px 14px;font-size:11px;line-height:1.5;color:#c7c4d7;
|
|
143
|
+
width:280px;pointer-events:none;opacity:0;transition:opacity 0.15s;
|
|
144
|
+
z-index:50;text-transform:none;letter-spacing:0;font-weight:400;
|
|
145
|
+
box-shadow:0 8px 24px rgba(0,0,0,0.4);
|
|
146
|
+
}
|
|
147
|
+
.has-tip:hover .tip{opacity:1;pointer-events:auto;}
|
|
148
|
+
|
|
149
|
+
/* Tooltip */
|
|
150
|
+
.tt {
|
|
151
|
+
position: fixed;
|
|
152
|
+
background: rgba(31, 32, 33, 0.95);
|
|
153
|
+
backdrop-filter: blur(12px);
|
|
154
|
+
border: 1px solid rgba(70, 69, 84, 0.3);
|
|
155
|
+
border-radius: 0.5rem;
|
|
156
|
+
padding: 10px 14px;
|
|
157
|
+
pointer-events: none;
|
|
158
|
+
opacity: 0;
|
|
159
|
+
transition: opacity 0.12s;
|
|
160
|
+
z-index: 100;
|
|
161
|
+
white-space: nowrap;
|
|
162
|
+
}
|
|
163
|
+
.tt.on { opacity: 1; }
|
|
164
|
+
|
|
165
|
+
/* Toast */
|
|
166
|
+
.toast {
|
|
167
|
+
position: fixed;
|
|
168
|
+
bottom: 20px;
|
|
169
|
+
left: 50%;
|
|
170
|
+
transform: translateX(-50%) translateY(60px);
|
|
171
|
+
background: #292a2b;
|
|
172
|
+
border: 1px solid rgba(70, 69, 84, 0.3);
|
|
173
|
+
border-radius: 0.5rem;
|
|
174
|
+
padding: 10px 20px;
|
|
175
|
+
font-size: 12px;
|
|
176
|
+
font-weight: 600;
|
|
177
|
+
color: #e3e2e3;
|
|
178
|
+
opacity: 0;
|
|
179
|
+
transition: all 0.25s;
|
|
180
|
+
z-index: 200;
|
|
181
|
+
pointer-events: none;
|
|
182
|
+
}
|
|
183
|
+
.toast.on { opacity: 1; transform: translateX(-50%) translateY(0); }
|
|
184
|
+
|
|
185
|
+
/* SVG chart */
|
|
186
|
+
#cost-chart-svg { width: 100%; overflow: visible; display: block; }
|
|
187
|
+
|
|
188
|
+
/* Table hover */
|
|
189
|
+
.tbl-row:hover { background: #292a2b; }
|
|
190
|
+
.tbl-row { transition: background 0.15s; }
|
|
191
|
+
</style>
|
|
192
|
+
</head>
|
|
193
|
+
<body class="selection:bg-primary selection:text-on-primary">
|
|
194
|
+
|
|
195
|
+
<div class="tt" id="tt">
|
|
196
|
+
<div class="font-mono text-[11px] text-[#908fa0] mb-1" id="tt-d"></div>
|
|
197
|
+
<div class="font-mono text-[15px] font-bold text-[#e3e2e3]" id="tt-c"></div>
|
|
198
|
+
<div class="text-[10px] text-[#ffb4ab] mt-1" id="tt-a"></div>
|
|
199
|
+
</div>
|
|
200
|
+
<div class="toast" id="toast"></div>
|
|
201
|
+
|
|
202
|
+
<!-- 1. HEADER -->
|
|
203
|
+
<header class="w-full px-6 py-5 max-w-[1200px] mx-auto flex justify-between items-baseline">
|
|
204
|
+
<div class="flex items-baseline gap-4">
|
|
205
|
+
<a href="https://github.com/azkhh/cchubber" target="_blank" class="text-lg font-bold tracking-tight text-[#e3e2e3]" style="text-decoration:none;">CC Hubber</a>
|
|
206
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0]">shipped fast with <a href="https://moveros.dev" target="_blank" style="text-decoration:none;color:inherit;">Mover OS</a></span>
|
|
207
|
+
</div>
|
|
208
|
+
<span class="font-mono text-[11px] text-[#908fa0]" id="range-lbl">All time</span>
|
|
209
|
+
</header>
|
|
210
|
+
|
|
211
|
+
<main class="pb-20 px-6 max-w-[1200px] mx-auto space-y-12">
|
|
212
|
+
|
|
213
|
+
<!-- 2. SHARE CARD — HTML for display, Canvas for video export -->
|
|
214
|
+
<section class="flex flex-col items-center">
|
|
215
|
+
<style>
|
|
216
|
+
@keyframes cardFloat{
|
|
217
|
+
0%,100%{transform:perspective(800px) rotateY(-2deg) rotateX(1deg)}
|
|
218
|
+
50%{transform:perspective(800px) rotateY(2deg) rotateX(-1deg)}
|
|
219
|
+
}
|
|
220
|
+
@keyframes shimmer{0%{background-position:-200% 0}100%{background-position:200% 0}}
|
|
221
|
+
.cc-card{
|
|
222
|
+
position:relative;width:100%;max-width:740px;
|
|
223
|
+
border-radius:22px;overflow:hidden;
|
|
224
|
+
background:linear-gradient(145deg,#1a1b2e 0%,#151622 20%,#0f1018 40%,#131428 55%,#191a2d 70%,#141520 85%,#12131f 100%);
|
|
225
|
+
box-shadow:0 2px 4px rgba(0,0,0,0.1),0 8px 16px rgba(0,0,0,0.1),0 16px 32px rgba(0,0,0,0.15);
|
|
226
|
+
animation:cardFloat 6s ease-in-out infinite;
|
|
227
|
+
}
|
|
228
|
+
.cc-card::before{
|
|
229
|
+
content:'';position:absolute;inset:0;
|
|
230
|
+
background:linear-gradient(105deg,transparent 30%,rgba(192,193,255,0.04) 45%,rgba(212,187,255,0.06) 50%,rgba(192,193,255,0.04) 55%,transparent 70%);
|
|
231
|
+
background-size:200% 100%;animation:shimmer 4s ease-in-out infinite;
|
|
232
|
+
pointer-events:none;z-index:2;
|
|
233
|
+
}
|
|
234
|
+
.cc-card::after{
|
|
235
|
+
content:'';position:absolute;inset:0;z-index:1;pointer-events:none;opacity:0.035;
|
|
236
|
+
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");
|
|
237
|
+
background-size:128px 128px;
|
|
238
|
+
}
|
|
239
|
+
.cc-inner{position:relative;z-index:3;display:flex;flex-direction:column;justify-content:space-between;padding:36px 40px;min-height:280px;}
|
|
240
|
+
.cc-card.no-shimmer::before{display:none!important;}
|
|
241
|
+
</style>
|
|
242
|
+
<div class="cc-card" id="share-card-html">
|
|
243
|
+
<div class="cc-inner">
|
|
244
|
+
<div class="flex items-start justify-between">
|
|
245
|
+
<div class="flex items-center gap-4">
|
|
246
|
+
<div class="w-14 h-14 flex items-center justify-center rounded-[12px]" style="background:${gradeColor}">
|
|
247
|
+
<span class="text-[30px]" style="color:#0f1018;font-weight:900;">${grade.letter}</span>
|
|
248
|
+
</div>
|
|
249
|
+
<div>
|
|
250
|
+
<span class="text-[10px] font-mono uppercase tracking-[0.08em] font-bold block" style="color:${gradeColor}">${grade.label}</span>
|
|
251
|
+
<span class="text-xl font-bold text-[#e3e2e3]">${gradeLabel}</span>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
254
|
+
<div class="text-right">
|
|
255
|
+
<span class="text-[11px] font-mono uppercase tracking-[0.1em] text-[#908fa0] font-bold block">Claude Code</span>
|
|
256
|
+
<span class="text-[11px] font-mono uppercase tracking-[0.06em] text-[#596678] block" id="card-range">All time</span>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
<div class="flex justify-between items-end">
|
|
260
|
+
<div>
|
|
261
|
+
<p class="text-[10px] uppercase tracking-[0.06em] text-[#908fa0] mb-1">Total Spend</p>
|
|
262
|
+
<p class="font-mono text-[40px] font-bold text-[#e3e2e3] leading-none" id="h-cost">${fmtCost(totalCost)}</p>
|
|
263
|
+
</div>
|
|
264
|
+
<div class="text-center">
|
|
265
|
+
<p class="text-[10px] uppercase tracking-[0.06em] text-[#908fa0] mb-1">Active Days</p>
|
|
266
|
+
<p class="font-mono text-[40px] font-bold text-[#e3e2e3] leading-none" id="h-days">${activeDays}</p>
|
|
267
|
+
</div>
|
|
268
|
+
<div class="text-right">
|
|
269
|
+
<p class="text-[10px] uppercase tracking-[0.06em] text-[#908fa0] mb-1">Cache Ratio</p>
|
|
270
|
+
<p class="font-mono text-[40px] font-bold text-[#e3e2e3] leading-none">${cacheHealth.efficiencyRatio ? cacheHealth.efficiencyRatio.toLocaleString() + ':1' : 'N/A'}</p>
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
<div class="flex justify-between items-end">
|
|
274
|
+
<p class="text-[12px] text-[#908fa0]">${diagnosisLine}</p>
|
|
275
|
+
<div class="flex items-center gap-2 text-[12px] font-mono tracking-[0.03em] shrink-0">
|
|
276
|
+
<a href="https://github.com/azkhh/cchubber" target="_blank" class="text-[#c0c1ff] hover:text-[#e1e0ff]" style="text-decoration:none;font-weight:600;">CC Hubber</a>
|
|
277
|
+
<span class="text-[#464554]">·</span>
|
|
278
|
+
<span class="text-[#908fa0]">shipped fast with</span>
|
|
279
|
+
<a href="https://moveros.dev" target="_blank" class="text-[#c0c1ff] hover:text-[#e1e0ff]" style="text-decoration:none;font-weight:600;">Mover OS</a>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
</div>
|
|
284
|
+
<!-- Hidden canvas for video recording -->
|
|
285
|
+
<canvas id="share-card" style="display:none;"></canvas>
|
|
286
|
+
<div class="flex justify-center mt-5">
|
|
287
|
+
<button id="btn-gif" class="px-5 py-2 border border-[rgba(70,69,84,0.3)] rounded-lg text-xs font-semibold text-[#908fa0] hover:bg-[#292a2b] hover:text-[#e3e2e3] transition-colors flex items-center gap-2 cursor-pointer">
|
|
288
|
+
<span class="material-symbols-outlined text-sm">share</span>
|
|
289
|
+
Share
|
|
290
|
+
</button>
|
|
291
|
+
</div>
|
|
292
|
+
</section>
|
|
293
|
+
|
|
294
|
+
${inflection && inflection.multiplier >= 1.5 ? `
|
|
295
|
+
<!-- Inflection callouts -->
|
|
296
|
+
<section class="space-y-3">
|
|
297
|
+
<div class="p-6 bg-[#0d0e0f] border-l-4 rounded-r-xl" style="border-left-color:${inflection.direction === 'worsened' ? '#ffb4ab' : '#c0c1ff'}">
|
|
298
|
+
<p class="text-xs font-bold uppercase tracking-[0.05em] mb-1" style="color:${inflection.direction === 'worsened' ? '#ffb4ab' : '#c0c1ff'}">${inflection.direction === 'worsened' ? 'Degradation Detected' : 'Inflection Point'}</p>
|
|
299
|
+
<p class="text-sm text-[#c7c4d7]">${inflection.summary}</p>
|
|
300
|
+
</div>
|
|
301
|
+
${inflection.secondary ? `
|
|
302
|
+
<div class="p-6 bg-[#0d0e0f] border-l-4 rounded-r-xl" style="border-left-color:${inflection.secondary.direction === 'worsened' ? '#ffb4ab' : '#c0c1ff'}">
|
|
303
|
+
<p class="text-xs font-bold uppercase tracking-[0.05em] mb-1" style="color:${inflection.secondary.direction === 'worsened' ? '#ffb4ab' : '#c0c1ff'}">${inflection.secondary.direction === 'worsened' ? 'Degradation Detected' : 'Recovery Detected'}</p>
|
|
304
|
+
<p class="text-sm text-[#c7c4d7]">${inflection.secondary.summary}</p>
|
|
305
|
+
</div>` : ''}
|
|
306
|
+
</section>
|
|
307
|
+
` : ''}
|
|
308
|
+
|
|
309
|
+
<!-- 3. METRIC GRID -->
|
|
310
|
+
<section class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 rounded-xl border border-[rgba(70,69,84,0.15)]" style="gap:1px; background:rgba(70,69,84,0.15);">
|
|
311
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
312
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">Total Cost</span>
|
|
313
|
+
<span class="font-mono text-2xl font-bold block text-[#e3e2e3]" id="ov-total">${fmtCost(totalCost)}</span>
|
|
314
|
+
<span class="text-[10px] text-[#908fa0] mt-1 block font-mono" id="ov-avg">${fmtCost(costAnalysis.avgDailyCost || 0)} avg/day</span>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
317
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">Peak Day</span>
|
|
318
|
+
<span class="font-mono text-2xl font-bold block text-[#e3e2e3]">${peakDay ? fmtCost(peakDay.cost) : '$0'}</span>
|
|
319
|
+
<span class="text-[10px] text-[#908fa0] mt-1 block font-mono">${peakDay ? peakDay.date : ''}</span>
|
|
320
|
+
</div>
|
|
321
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
322
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3 has-tip">Cache Health <span class="text-[8px] text-[#464554]">ⓘ</span>
|
|
323
|
+
<span class="tip">Overall grade based on your cache efficiency ratio, weighted towards recent 7 days. A-B = healthy (300-800:1 ratio). C = elevated, investigate. D-F = critical, likely affected by cache bugs. The grade drops when recent efficiency is worse than historical.</span>
|
|
324
|
+
</span>
|
|
325
|
+
<span class="font-mono text-2xl font-bold block" style="color:${gradeColor}">${grade.letter}</span>
|
|
326
|
+
<span class="text-[10px] text-[#908fa0] mt-1 block font-mono">${cacheHealth.efficiencyRatio ? cacheHealth.efficiencyRatio.toLocaleString() + ':1' : ''}</span>
|
|
327
|
+
</div>
|
|
328
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
329
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3 has-tip">Cache Breaks <span class="text-[8px] text-[#464554]">ⓘ</span>
|
|
330
|
+
<span class="tip">When your prompt cache is invalidated, Claude Code re-reads your entire context at 12.5x the cached price. Causes: editing CLAUDE.md mid-session, connecting/disconnecting MCP tools, model switches, 5-min inactivity timeout. ${cacheHealth.totalCacheBreaks > 0 ? 'Counted from cache-break diff files.' : 'Estimated from cache write tokens since no diff files exist on your CC version.'}</span>
|
|
331
|
+
</span>
|
|
332
|
+
<span class="font-mono text-2xl font-bold block text-[#e3e2e3]">${cacheHealth.totalCacheBreaks > 0 ? cacheHealth.totalCacheBreaks : '~' + (cacheHealth.estimatedBreaks || 0)}</span>
|
|
333
|
+
<span class="text-[10px] text-[#908fa0] mt-1 block">${cacheHealth.totalCacheBreaks > 0 ? cacheHealth.reasonsRanked?.[0]?.reason : 'estimated from writes'}</span>
|
|
334
|
+
</div>
|
|
335
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
336
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">CLAUDE.md</span>
|
|
337
|
+
<span class="font-mono text-2xl font-bold block text-[#e3e2e3]">~${Math.round(claudeMdStack.totalTokensEstimate / 1000)}K</span>
|
|
338
|
+
<span class="text-[10px] text-[#908fa0] mt-1 block font-mono">${(claudeMdStack.totalBytes / 1024).toFixed(1)} KB</span>
|
|
339
|
+
</div>
|
|
340
|
+
${sessionIntel?.available ? `
|
|
341
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
342
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">Sessions</span>
|
|
343
|
+
<span class="font-mono text-2xl font-bold block text-[#e3e2e3]">${sessionIntel.totalSessions}</span>
|
|
344
|
+
<span class="text-[10px] text-[#908fa0] mt-1 block font-mono">${sessionIntel.avgDuration} min avg</span>
|
|
345
|
+
</div>` : `
|
|
346
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
347
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">Sessions</span>
|
|
348
|
+
<span class="font-mono text-2xl font-bold block text-[#e3e2e3]">${costAnalysis.sessions?.total || 0}</span>
|
|
349
|
+
<span class="text-[10px] text-[#908fa0] mt-1 block font-mono">${costAnalysis.sessions?.avgDurationMinutes ? Math.round(costAnalysis.sessions.avgDurationMinutes) + ' min avg' : ''}</span>
|
|
350
|
+
</div>`}
|
|
351
|
+
</section>
|
|
352
|
+
|
|
353
|
+
<!-- 4. COST TREND CHART -->
|
|
354
|
+
<section class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
|
|
355
|
+
<div class="flex justify-between items-end mb-10">
|
|
356
|
+
<div>
|
|
357
|
+
<h3 class="text-xl font-bold text-[#e3e2e3] mb-1">Cost Trend</h3>
|
|
358
|
+
<p class="text-sm text-[#908fa0]" id="chart-info"></p>
|
|
359
|
+
</div>
|
|
360
|
+
<div class="flex gap-1 p-1 bg-[#0d0e0f] rounded-xl border border-[rgba(70,69,84,0.15)]" id="filters">
|
|
361
|
+
<button class="cfilt px-3 py-1.5 text-[10px] font-bold uppercase rounded-lg text-[#908fa0] hover:text-[#e3e2e3] transition-colors" data-r="7">7d</button>
|
|
362
|
+
<button class="cfilt px-3 py-1.5 text-[10px] font-bold uppercase rounded-lg text-[#908fa0] hover:text-[#e3e2e3] transition-colors" data-r="30">30d</button>
|
|
363
|
+
<button class="cfilt px-3 py-1.5 text-[10px] font-bold uppercase rounded-lg text-[#908fa0] hover:text-[#e3e2e3] transition-colors" data-r="90">90d</button>
|
|
364
|
+
<button class="cfilt px-3 py-1.5 text-[10px] font-bold uppercase rounded-lg bg-[#c0c1ff] text-[#1000a9] transition-colors" data-r="all">All</button>
|
|
365
|
+
</div>
|
|
366
|
+
</div>
|
|
367
|
+
<svg id="cost-chart-svg" viewBox="0 0 900 200" preserveAspectRatio="xMidYMid meet"></svg>
|
|
368
|
+
</section>
|
|
369
|
+
|
|
370
|
+
<!-- 5. SESSION INTELLIGENCE + MODEL DISTRIBUTION -->
|
|
371
|
+
<section class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
|
372
|
+
|
|
373
|
+
<!-- Session Intelligence -->
|
|
374
|
+
<div class="space-y-8">
|
|
375
|
+
${sessionIntel?.available ? `
|
|
376
|
+
<div class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
|
|
377
|
+
<h3 class="text-xl font-bold text-[#e3e2e3] mb-6">Session Intelligence</h3>
|
|
378
|
+
<div class="grid grid-cols-3 gap-6 mb-8">
|
|
379
|
+
<div>
|
|
380
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">Median</span>
|
|
381
|
+
<span class="font-mono text-xl font-bold text-[#e3e2e3]">${fmtDuration(sessionIntel.medianDuration)}</span>
|
|
382
|
+
</div>
|
|
383
|
+
<div>
|
|
384
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">P90</span>
|
|
385
|
+
<span class="font-mono text-xl font-bold text-[#e3e2e3]">${fmtDuration(sessionIntel.p90Duration)}</span>
|
|
386
|
+
</div>
|
|
387
|
+
<div>
|
|
388
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">Longest</span>
|
|
389
|
+
<span class="font-mono text-xl font-bold text-[#e3e2e3]">${fmtDuration(sessionIntel.maxDuration)}</span>
|
|
390
|
+
</div>
|
|
391
|
+
</div>
|
|
392
|
+
<div class="grid grid-cols-3 gap-6 mb-8">
|
|
393
|
+
<div>
|
|
394
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">Long Sessions</span>
|
|
395
|
+
<span class="font-mono text-xl font-bold text-[#e3e2e3]">${sessionIntel.longSessions}</span>
|
|
396
|
+
<span class="text-[10px] text-[#908fa0] block font-mono">${sessionIntel.longSessionPct}% over 60m</span>
|
|
397
|
+
</div>
|
|
398
|
+
<div>
|
|
399
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">Tools/Session</span>
|
|
400
|
+
<span class="font-mono text-xl font-bold text-[#e3e2e3]">${sessionIntel.avgToolsPerSession}</span>
|
|
401
|
+
</div>
|
|
402
|
+
<div>
|
|
403
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-1">Lines/Hour</span>
|
|
404
|
+
<span class="font-mono text-xl font-bold text-[#e3e2e3]">${sessionIntel.linesPerHour.toLocaleString()}</span>
|
|
405
|
+
</div>
|
|
406
|
+
</div>
|
|
407
|
+
|
|
408
|
+
${sessionIntel.topTools.length > 0 ? `
|
|
409
|
+
<div>
|
|
410
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-4">Top Tools Usage</span>
|
|
411
|
+
<div class="space-y-3">
|
|
412
|
+
${sessionIntel.topTools.slice(0, 6).map((t, i) => `
|
|
413
|
+
<div class="space-y-1">
|
|
414
|
+
<div class="flex justify-between text-[11px] font-mono">
|
|
415
|
+
<span class="text-[#c7c4d7]">${t.name}</span>
|
|
416
|
+
<span class="text-[#908fa0]">${t.count}</span>
|
|
417
|
+
</div>
|
|
418
|
+
<div class="h-1.5 bg-[#343536] rounded-full overflow-hidden">
|
|
419
|
+
<div class="h-full rounded-full" style="width:${sessionIntel.topTools[0].count > 0 ? (t.count / sessionIntel.topTools[0].count * 100) : 0}%;background:${i === 0 ? '#c0c1ff' : '#d4bbff'}"></div>
|
|
420
|
+
</div>
|
|
421
|
+
</div>`).join('')}
|
|
422
|
+
</div>
|
|
423
|
+
</div>` : ''}
|
|
424
|
+
</div>` : ''}
|
|
425
|
+
|
|
426
|
+
<!-- 6. ACTIVITY HEATMAP -->
|
|
427
|
+
${sessionIntel?.hourDistribution ? `
|
|
428
|
+
<div class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
|
|
429
|
+
<h3 class="text-lg font-bold text-[#e3e2e3] mb-4">Activity by Hour</h3>
|
|
430
|
+
<div class="" id="hour-grid" style="display:flex;justify-content:space-between;align-items:flex-end;padding:0 4px;"></div>
|
|
431
|
+
<!-- labels rendered by JS -->
|
|
432
|
+
</div>` : ''}
|
|
433
|
+
</div>
|
|
434
|
+
|
|
435
|
+
<!-- Model Distribution -->
|
|
436
|
+
<div class="space-y-8">
|
|
437
|
+
<div class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
|
|
438
|
+
<h3 class="text-xl font-bold text-[#e3e2e3] mb-6">Model Distribution</h3>
|
|
439
|
+
<div class="w-full h-4 flex rounded-full overflow-hidden mb-6" style="gap:2px">
|
|
440
|
+
${modelEntries.map(([, cost], i) => {
|
|
441
|
+
const pct = totalCost > 0 ? (cost / totalCost) * 100 : 0;
|
|
442
|
+
return `<div class="h-full" style="width:${pct}%;background:${modelColors[i % modelColors.length]};border-radius:${i === 0 ? '9999px 0 0 9999px' : i === modelEntries.length - 1 ? '0 9999px 9999px 0' : '0'}"></div>`;
|
|
443
|
+
}).join('')}
|
|
444
|
+
</div>
|
|
445
|
+
<div class="grid grid-cols-2 gap-4">
|
|
446
|
+
${modelEntries.map(([name, cost], i) => {
|
|
447
|
+
const pct = totalCost > 0 ? ((cost / totalCost) * 100).toFixed(1) : '0';
|
|
448
|
+
return `<div class="flex items-center gap-2">
|
|
449
|
+
<div class="w-2 h-2 rounded-full" style="background:${modelColors[i % modelColors.length]}"></div>
|
|
450
|
+
<span class="text-xs font-mono text-[#c7c4d7]">${name}</span>
|
|
451
|
+
<span class="text-xs font-mono text-[#908fa0]">${fmtCost(cost)}</span>
|
|
452
|
+
<span class="text-[10px] text-[#464554] font-mono">${pct}%</span>
|
|
453
|
+
</div>`;
|
|
454
|
+
}).join('')}
|
|
455
|
+
</div>
|
|
456
|
+
${modelRouting?.available ? `
|
|
457
|
+
<div class="mt-6 pt-6 border-t border-[rgba(70,69,84,0.15)] text-sm text-[#c7c4d7]">
|
|
458
|
+
<span class="font-mono">${modelRouting.opusPct}%</span> Opus ·
|
|
459
|
+
<span class="font-mono">${modelRouting.sonnetPct}%</span> Sonnet ·
|
|
460
|
+
<span class="font-mono">${modelRouting.haikuPct}%</span> Haiku
|
|
461
|
+
${modelRouting.estimatedSavings > 10 ? `<span class="text-[#c0c1ff] ml-3 font-mono">~${fmtCost(modelRouting.estimatedSavings)} potential savings</span>` : ''}
|
|
462
|
+
</div>` : ''}
|
|
463
|
+
</div>
|
|
464
|
+
|
|
465
|
+
<!-- 9. RECOMMENDATIONS (placed alongside model distribution) -->
|
|
466
|
+
${recommendations.length > 0 ? `
|
|
467
|
+
<div class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
|
|
468
|
+
<h3 class="text-xl font-bold text-[#e3e2e3] mb-6">Recommendations</h3>
|
|
469
|
+
<div class="space-y-3">
|
|
470
|
+
${recommendations.map(r => {
|
|
471
|
+
const sev = sevColorMap[r.severity] || sevColorMap.info;
|
|
472
|
+
return `<div class="p-4 bg-[#0d0e0f] rounded-r-lg flex items-start gap-4" style="border-left:3px solid ${sev.border}">
|
|
473
|
+
<div class="flex-1 min-w-0">
|
|
474
|
+
<div class="flex items-start justify-between gap-4">
|
|
475
|
+
<p class="text-[13px] font-semibold text-[#e3e2e3]">${r.title}</p>
|
|
476
|
+
${r.savings ? `<span class="text-[10px] font-mono shrink-0 px-2 py-0.5 rounded" style="background:${sev.border}18;color:${sev.text}">${r.savings}</span>` : ''}
|
|
477
|
+
</div>
|
|
478
|
+
<p class="text-[11px] text-[#908fa0] mt-1 leading-relaxed">${r.action}</p>
|
|
479
|
+
</div>
|
|
480
|
+
</div>`;
|
|
481
|
+
}).join('')}
|
|
482
|
+
</div>
|
|
483
|
+
</div>` : ''}
|
|
484
|
+
</div>
|
|
485
|
+
</section>
|
|
486
|
+
|
|
487
|
+
<!-- 7. PROJECTS TABLE -->
|
|
488
|
+
${projectBreakdown && projectBreakdown.length > 0 ? `
|
|
489
|
+
<section class="bg-[#1b1c1d] rounded-xl border border-[rgba(70,69,84,0.15)] overflow-hidden">
|
|
490
|
+
<div class="px-8 py-6 border-b border-[rgba(70,69,84,0.15)]">
|
|
491
|
+
<h3 class="text-xl font-bold text-[#e3e2e3]">Projects</h3>
|
|
492
|
+
</div>
|
|
493
|
+
<div class="overflow-x-auto">
|
|
494
|
+
<table class="w-full text-left" id="proj-tbl">
|
|
495
|
+
<thead class="bg-[#0d0e0f] border-b border-[rgba(70,69,84,0.15)]">
|
|
496
|
+
<tr>
|
|
497
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Project</th>
|
|
498
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Messages</th>
|
|
499
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Sessions</th>
|
|
500
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Output</th>
|
|
501
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Cache Read</th>
|
|
502
|
+
</tr>
|
|
503
|
+
</thead>
|
|
504
|
+
<tbody class="divide-y divide-[rgba(70,69,84,0.15)]"></tbody>
|
|
505
|
+
</table>
|
|
506
|
+
</div>
|
|
507
|
+
</section>
|
|
508
|
+
` : ''}
|
|
509
|
+
|
|
510
|
+
<!-- 8. ANOMALIES TABLE -->
|
|
511
|
+
${anomalies.hasAnomalies ? `
|
|
512
|
+
<section class="bg-[#1b1c1d] rounded-xl border border-[rgba(70,69,84,0.15)] overflow-hidden">
|
|
513
|
+
<div class="px-8 py-6 border-b border-[rgba(70,69,84,0.15)] flex justify-between items-center">
|
|
514
|
+
<h3 class="text-xl font-bold text-[#e3e2e3]">Detected Anomalies</h3>
|
|
515
|
+
<span class="material-symbols-outlined text-[#ffb4ab] animate-pulse">warning</span>
|
|
516
|
+
</div>
|
|
517
|
+
<div class="overflow-x-auto">
|
|
518
|
+
<table class="w-full text-left">
|
|
519
|
+
<thead class="bg-[#0d0e0f] border-b border-[rgba(70,69,84,0.15)]">
|
|
520
|
+
<tr>
|
|
521
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Date</th>
|
|
522
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Cost</th>
|
|
523
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Deviation</th>
|
|
524
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Cache Ratio</th>
|
|
525
|
+
<th class="px-8 py-4 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Severity</th>
|
|
526
|
+
</tr>
|
|
527
|
+
</thead>
|
|
528
|
+
<tbody class="divide-y divide-[rgba(70,69,84,0.15)]">
|
|
529
|
+
${anomalies.anomalies.map(a => {
|
|
530
|
+
const sevBg = a.severity === 'critical' ? 'rgba(255, 180, 171, 0.10)' : 'rgba(255, 182, 144, 0.10)';
|
|
531
|
+
const sevText = a.severity === 'critical' ? '#ffb4ab' : '#ffb690';
|
|
532
|
+
return `<tr class="tbl-row">
|
|
533
|
+
<td class="px-8 py-4 font-mono text-sm text-[#e3e2e3]">${a.date}</td>
|
|
534
|
+
<td class="px-8 py-4 font-mono text-sm text-[#e3e2e3] font-bold">${fmtCost(a.cost)}</td>
|
|
535
|
+
<td class="px-8 py-4 font-mono text-sm font-bold" style="color:${a.deviation > 0 ? '#ffb4ab' : '#c0c1ff'}">${a.deviation > 0 ? '+' : ''}$${a.deviation.toFixed(2)}</td>
|
|
536
|
+
<td class="px-8 py-4 font-mono text-sm text-[#c7c4d7]">${a.cacheOutputRatio ? a.cacheOutputRatio.toLocaleString() + ':1' : ''}</td>
|
|
537
|
+
<td class="px-8 py-4 text-right"><span class="px-2 py-0.5 rounded text-[10px] font-bold font-mono uppercase" style="background:${sevBg};color:${sevText}">${a.severity}</span></td>
|
|
538
|
+
</tr>`;
|
|
539
|
+
}).join('')}
|
|
540
|
+
</tbody>
|
|
541
|
+
</table>
|
|
542
|
+
</div>
|
|
543
|
+
</section>
|
|
544
|
+
` : ''}
|
|
545
|
+
|
|
546
|
+
<!-- 10. CLAUDE.md ANALYSIS — Global only, section breakdown -->
|
|
547
|
+
<section class="bg-[#1b1c1d] rounded-xl border border-[rgba(70,69,84,0.15)] overflow-hidden">
|
|
548
|
+
<div class="px-8 py-6 border-b border-[rgba(70,69,84,0.15)] flex justify-between items-center">
|
|
549
|
+
<h3 class="text-xl font-bold text-[#e3e2e3]">CLAUDE.md Analysis</h3>
|
|
550
|
+
<div class="text-right">
|
|
551
|
+
<span class="font-mono text-sm text-[#e3e2e3]">${claudeMdStack.files[0]?.lineCount || '?'} lines</span>
|
|
552
|
+
<span class="text-[#908fa0] mx-2">·</span>
|
|
553
|
+
<span class="font-mono text-sm text-[#e3e2e3]">~${claudeMdStack.totalTokensEstimate.toLocaleString()} tokens</span>
|
|
554
|
+
<span class="text-[#908fa0] mx-2">·</span>
|
|
555
|
+
<span class="font-mono text-sm text-[#e3e2e3]">${(claudeMdStack.totalBytes / 1024).toFixed(1)} KB</span>
|
|
556
|
+
</div>
|
|
557
|
+
</div>
|
|
558
|
+
<div class="px-8 py-4 bg-[#0d0e0f] border-b border-[rgba(70,69,84,0.15)] flex justify-between text-xs">
|
|
559
|
+
<span class="text-[#908fa0]">Per-message cost impact</span>
|
|
560
|
+
<span class="font-mono">
|
|
561
|
+
<span class="text-[#c0c1ff]">$${claudeMdStack.costPerMessage.cached.toFixed(4)}</span> cached ·
|
|
562
|
+
<span class="text-[#ffb690]">$${claudeMdStack.costPerMessage.uncached.toFixed(4)}</span> uncached ·
|
|
563
|
+
<span class="text-[#ffb4ab]">$${(claudeMdStack.costPerMessage.dailyCached200 || 0).toFixed(2)}</span>/day at 200 msgs
|
|
564
|
+
</span>
|
|
565
|
+
</div>
|
|
566
|
+
${claudeMdStack.globalSections && claudeMdStack.globalSections.length > 0 ? `
|
|
567
|
+
<div class="overflow-x-auto">
|
|
568
|
+
<table class="w-full text-left">
|
|
569
|
+
<thead class="bg-[#0d0e0f] border-b border-[rgba(70,69,84,0.15)]">
|
|
570
|
+
<tr>
|
|
571
|
+
<th class="px-8 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0]">Section</th>
|
|
572
|
+
<th class="px-8 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Lines</th>
|
|
573
|
+
<th class="px-8 py-3 text-[10px] uppercase font-bold tracking-[0.05em] text-[#908fa0] text-right">Tokens</th>
|
|
574
|
+
</tr>
|
|
575
|
+
</thead>
|
|
576
|
+
<tbody class="divide-y divide-[rgba(70,69,84,0.15)]">
|
|
577
|
+
${claudeMdStack.globalSections.map(s => `<tr class="tbl-row">
|
|
578
|
+
<td class="px-8 py-3 text-sm text-[#e3e2e3]">${s.name}</td>
|
|
579
|
+
<td class="px-8 py-3 font-mono text-sm text-[#c7c4d7] text-right">${s.lines}</td>
|
|
580
|
+
<td class="px-8 py-3 font-mono text-sm text-[#c7c4d7] text-right">${s.tokens.toLocaleString()}</td>
|
|
581
|
+
</tr>`).join('')}
|
|
582
|
+
</tbody>
|
|
583
|
+
</table>
|
|
584
|
+
</div>
|
|
585
|
+
` : ''}
|
|
586
|
+
</section>
|
|
587
|
+
|
|
588
|
+
<!-- 11. CACHE SAVINGS -->
|
|
589
|
+
${cacheHealth.savings?.fromCaching > 0 ? `
|
|
590
|
+
<section class="grid grid-cols-1 md:grid-cols-2 rounded-xl overflow-hidden border border-[rgba(70,69,84,0.15)]" style="gap:1px; background:rgba(70,69,84,0.15);">
|
|
591
|
+
<div class="p-8 bg-[#0d0e0f]">
|
|
592
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">Saved by Cache</span>
|
|
593
|
+
<span class="font-mono text-3xl font-bold block text-[#c0c1ff]">~$${Number(cacheHealth.savings.fromCaching).toLocaleString()}</span>
|
|
594
|
+
<span class="text-[10px] text-[#908fa0] mt-2 block">vs standard input pricing</span>
|
|
595
|
+
</div>
|
|
596
|
+
<div class="p-8 bg-[#0d0e0f]">
|
|
597
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">Wasted on Breaks</span>
|
|
598
|
+
<span class="font-mono text-3xl font-bold block text-[#ffb690]">~$${Number(cacheHealth.savings.wastedFromBreaks).toLocaleString()}</span>
|
|
599
|
+
<span class="text-[10px] text-[#908fa0] mt-2 block">from cache invalidation</span>
|
|
600
|
+
</div>
|
|
601
|
+
</section>
|
|
602
|
+
` : ''}
|
|
603
|
+
|
|
604
|
+
${cacheHealth.totalCacheBreaks > 0 ? `
|
|
605
|
+
<!-- Cache Break Reasons -->
|
|
606
|
+
<section class="bg-[#1b1c1d] p-8 rounded-xl border border-[rgba(70,69,84,0.15)]">
|
|
607
|
+
<h3 class="text-xl font-bold text-[#e3e2e3] mb-6">Cache Break Reasons</h3>
|
|
608
|
+
<div class="space-y-4">
|
|
609
|
+
${(cacheHealth.reasonsRanked || []).map(r => `
|
|
610
|
+
<div class="space-y-1">
|
|
611
|
+
<div class="flex justify-between text-[11px] font-mono">
|
|
612
|
+
<span class="text-[#c7c4d7]">${r.reason}</span>
|
|
613
|
+
<span class="text-[#908fa0]">${r.count}</span>
|
|
614
|
+
</div>
|
|
615
|
+
<div class="h-1.5 bg-[#343536] rounded-full overflow-hidden">
|
|
616
|
+
<div class="h-full bg-[#ffb690] rounded-full" style="width:${r.percentage}%"></div>
|
|
617
|
+
</div>
|
|
618
|
+
</div>`).join('')}
|
|
619
|
+
</div>
|
|
620
|
+
</section>
|
|
621
|
+
` : ''}
|
|
622
|
+
|
|
623
|
+
</main>
|
|
624
|
+
|
|
625
|
+
<!-- 12. FOOTER -->
|
|
626
|
+
<footer class="w-full py-12 border-t border-[rgba(70,69,84,0.05)]">
|
|
627
|
+
<div class="max-w-[1200px] mx-auto px-6 text-center">
|
|
628
|
+
<span class="text-[10px] tracking-widest uppercase text-[#908fa0]"><a href="https://github.com/azkhh/cchubber" target="_blank" style="text-decoration:none;color:inherit;">CC Hubber</a> · shipped fast with <a href="https://moveros.dev" target="_blank" style="text-decoration:none;color:inherit;">Mover OS</a></span>
|
|
629
|
+
</div>
|
|
630
|
+
</footer>
|
|
631
|
+
|
|
632
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/html-to-image/1.11.11/html-to-image.min.js"></script>
|
|
633
|
+
<script>
|
|
634
|
+
(function(){
|
|
635
|
+
var D=${dailyCostsJSON}, P=${projectsJSON};
|
|
636
|
+
var CARD={
|
|
637
|
+
grade:'${grade.letter}',gradeLabel:'${grade.label}',gradePerf:'${gradeLabel}',
|
|
638
|
+
gradeColor:'${gradeColor}',
|
|
639
|
+
cost:'${fmtCost(totalCost)}',days:'${activeDays}',
|
|
640
|
+
ratio:'${cacheHealth.efficiencyRatio ? cacheHealth.efficiencyRatio.toLocaleString() + ':1' : 'N/A'}',
|
|
641
|
+
diagnosis:'${diagnosisLine.replace(/'/g, "\\'")}',
|
|
642
|
+
range:'All time'
|
|
643
|
+
};
|
|
644
|
+
var HR=${sessionIntel?.hourDistribution ? JSON.stringify(sessionIntel.hourDistribution) : 'null'};
|
|
645
|
+
var CACHE_R=0.50,OUT=25,INP=5,CW=6.25;
|
|
646
|
+
|
|
647
|
+
function fc(n){return n>=100?'$'+Math.round(n).toLocaleString():'$'+n.toFixed(2)}
|
|
648
|
+
function ft(n){return n>=1e9?(n/1e9).toFixed(1)+'B':n>=1e6?(n/1e6).toFixed(1)+'M':n>=1e3?(n/1e3).toFixed(1)+'K':n.toString()}
|
|
649
|
+
|
|
650
|
+
// Hour activity — vertical bar chart
|
|
651
|
+
if(HR){
|
|
652
|
+
var hg=document.getElementById('hour-grid');
|
|
653
|
+
if(hg){
|
|
654
|
+
var mx=Math.max.apply(null,HR);
|
|
655
|
+
var html='';
|
|
656
|
+
for(var i=0;i<24;i++){
|
|
657
|
+
var pct=mx>0?Math.max(HR[i]/mx*100,2):2;
|
|
658
|
+
var opac=pct>70?'0.85':pct>40?'0.6':pct>15?'0.35':'0.12';
|
|
659
|
+
html+='<div style="display:flex;flex-direction:column;align-items:center;gap:4px;" title="'+i+':00 — '+HR[i]+' messages">';
|
|
660
|
+
html+='<div style="width:3px;height:80px;background:rgba(70,69,84,0.2);border-radius:2px;position:relative;overflow:hidden;">';
|
|
661
|
+
html+='<div style="position:absolute;bottom:0;width:100%;height:'+pct+'%;background:rgba(192,193,255,'+opac+');border-radius:2px;"></div>';
|
|
662
|
+
html+='</div>';
|
|
663
|
+
html+='<span style="font-size:8px;font-family:JetBrains Mono,monospace;color:'+(i%6===0?'#908fa0':'#464554')+';">'+i+'</span>';
|
|
664
|
+
html+='</div>';
|
|
665
|
+
}
|
|
666
|
+
hg.innerHTML=html;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// Project table
|
|
671
|
+
var ptb=document.querySelector('#proj-tbl tbody');
|
|
672
|
+
if(ptb&&P.length>0){
|
|
673
|
+
P.sort(function(a,b){return(b.output/1e6*OUT+b.cacheRead/1e6*CACHE_R)-(a.output/1e6*OUT+a.cacheRead/1e6*CACHE_R)});
|
|
674
|
+
var h='';
|
|
675
|
+
for(var i=0;i<Math.min(P.length,10);i++){
|
|
676
|
+
var p=P[i];
|
|
677
|
+
h+='<tr class="tbl-row">';
|
|
678
|
+
h+='<td class="px-8 py-4 text-sm font-semibold text-[#e3e2e3]">'+p.name;
|
|
679
|
+
if(p.path)h+='<br><span class="text-[10px] text-[#908fa0] font-mono">'+p.path+'</span>';
|
|
680
|
+
h+='</td>';
|
|
681
|
+
h+='<td class="px-8 py-4 font-mono text-sm text-[#c7c4d7]">'+p.messages.toLocaleString()+'</td>';
|
|
682
|
+
h+='<td class="px-8 py-4 font-mono text-sm text-[#c7c4d7]">'+p.sessions+'</td>';
|
|
683
|
+
h+='<td class="px-8 py-4 font-mono text-sm text-[#c7c4d7]">'+ft(p.output)+'</td>';
|
|
684
|
+
h+='<td class="px-8 py-4 font-mono text-sm text-[#c7c4d7] text-right">'+ft(p.cacheRead)+'</td>';
|
|
685
|
+
h+='</tr>';
|
|
686
|
+
}
|
|
687
|
+
ptb.innerHTML=h;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Chart
|
|
691
|
+
var W=900,H=200,PD={t:24,r:16,b:40,l:56};
|
|
692
|
+
var cW=W-PD.l-PD.r,cH=H-PD.t-PD.b;
|
|
693
|
+
var svg=document.getElementById('cost-chart-svg');
|
|
694
|
+
var tt=document.getElementById('tt'),ttd=document.getElementById('tt-d'),ttc=document.getElementById('tt-c'),tta=document.getElementById('tt-a');
|
|
695
|
+
|
|
696
|
+
function filt(r){return r==='all'?D:D.slice(-parseInt(r,10))}
|
|
697
|
+
|
|
698
|
+
function chart(d){
|
|
699
|
+
if(!svg)return;
|
|
700
|
+
if(!d.length){svg.innerHTML='<text x="450" y="100" text-anchor="middle" fill="#908fa0" font-size="13" font-family="Inter,sans-serif">No data</text>';return}
|
|
701
|
+
var mx=Math.max.apply(null,d.map(function(x){return x.cost}))*1.1;if(mx<0.01)mx=1;
|
|
702
|
+
var s='';
|
|
703
|
+
// grid lines
|
|
704
|
+
for(var i=0;i<=3;i++){
|
|
705
|
+
var y=PD.t+(cH/3)*i,v=mx-(mx/3)*i;
|
|
706
|
+
s+='<line x1="'+PD.l+'" y1="'+y+'" x2="'+(W-PD.r)+'" y2="'+y+'" stroke="rgba(70,69,84,0.15)" stroke-width="1"/>';
|
|
707
|
+
s+='<text x="'+(PD.l-10)+'" y="'+(y+4)+'" text-anchor="end" fill="#908fa0" font-size="9" font-family="JetBrains Mono,monospace">$'+(v<1?v.toFixed(2):Math.round(v))+'</text>';
|
|
708
|
+
}
|
|
709
|
+
// area + line
|
|
710
|
+
var step=d.length>1?cW/(d.length-1):0;
|
|
711
|
+
var pts=d.map(function(x,j){return{x:PD.l+(d.length===1?cW/2:j*step),y:PD.t+cH-(x.cost/mx)*cH}});
|
|
712
|
+
var lp='M '+pts[0].x+' '+pts[0].y;
|
|
713
|
+
var ap='M '+pts[0].x+' '+(PD.t+cH)+' L '+pts[0].x+' '+pts[0].y;
|
|
714
|
+
for(var j=1;j<pts.length;j++){var cx=(pts[j-1].x+pts[j].x)/2;lp+=' C '+cx+' '+pts[j-1].y+' '+cx+' '+pts[j].y+' '+pts[j].x+' '+pts[j].y;ap+=' C '+cx+' '+pts[j-1].y+' '+cx+' '+pts[j].y+' '+pts[j].x+' '+pts[j].y}
|
|
715
|
+
ap+=' L '+pts[pts.length-1].x+' '+(PD.t+cH)+' Z';
|
|
716
|
+
// Stitch gradient: primary at 30% to transparent
|
|
717
|
+
s+='<defs><linearGradient id="ag" x1="0" y1="0" x2="0" y2="1"><stop offset="0%" stop-color="#c0c1ff" stop-opacity="0.3"/><stop offset="100%" stop-color="#c0c1ff" stop-opacity="0"/></linearGradient></defs>';
|
|
718
|
+
s+='<path d="'+ap+'" fill="url(#ag)"/>';
|
|
719
|
+
s+='<path d="'+lp+'" fill="none" stroke="#c0c1ff" stroke-width="2" stroke-linecap="round"/>';
|
|
720
|
+
// x labels
|
|
721
|
+
var every=Math.max(1,Math.floor(d.length/8));
|
|
722
|
+
d.forEach(function(x,j){
|
|
723
|
+
var px=PD.l+(d.length===1?cW/2:j*step);
|
|
724
|
+
if(j%every===0||j===d.length-1)s+='<text x="'+px+'" y="'+(H-6)+'" text-anchor="middle" fill="#908fa0" font-size="9" font-family="JetBrains Mono,monospace">'+x.date.slice(5)+'</text>';
|
|
725
|
+
});
|
|
726
|
+
// anomaly dots
|
|
727
|
+
d.forEach(function(x,j){
|
|
728
|
+
var px=PD.l+(d.length===1?cW/2:j*step),py=PD.t+cH-(x.cost/mx)*cH;
|
|
729
|
+
if(x.isAnomaly)s+='<circle cx="'+px+'" cy="'+py+'" r="4" fill="#ffb4ab" stroke="#121315" stroke-width="2"/>';
|
|
730
|
+
});
|
|
731
|
+
// hover targets
|
|
732
|
+
d.forEach(function(x,j){
|
|
733
|
+
var px=PD.l+(d.length===1?cW/2:j*step),py=PD.t+cH-(x.cost/mx)*cH;
|
|
734
|
+
s+='<circle cx="'+px+'" cy="'+py+'" r="14" fill="transparent" data-d="'+x.date+'" data-c="'+x.cost+'" data-a="'+(x.isAnomaly?1:0)+'" class="hov" style="cursor:crosshair"/>';
|
|
735
|
+
});
|
|
736
|
+
svg.innerHTML=s;
|
|
737
|
+
svg.querySelectorAll('.hov').forEach(function(el){
|
|
738
|
+
el.addEventListener('mouseenter',function(e){
|
|
739
|
+
ttd.textContent=e.target.dataset.d;
|
|
740
|
+
ttc.textContent=fc(parseFloat(e.target.dataset.c));
|
|
741
|
+
tta.textContent=e.target.dataset.a==='1'?'ANOMALY':'';
|
|
742
|
+
tta.style.display=e.target.dataset.a==='1'?'block':'none';
|
|
743
|
+
tt.classList.add('on');
|
|
744
|
+
});
|
|
745
|
+
el.addEventListener('mousemove',function(e){tt.style.left=(e.clientX+14)+'px';tt.style.top=(e.clientY-40)+'px'});
|
|
746
|
+
el.addEventListener('mouseleave',function(){tt.classList.remove('on')});
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
var RL={7:'Last 7 days',30:'Last 30 days',90:'Last 90 days',all:'All time'};
|
|
751
|
+
|
|
752
|
+
function setR(r){
|
|
753
|
+
var f=filt(r);chart(f);
|
|
754
|
+
var ci=document.getElementById('chart-info');
|
|
755
|
+
if(ci&&f.length){var t=f.reduce(function(s,x){return s+x.cost},0),a=f.filter(function(x){return x.cost>0}).length;ci.textContent=a+' days \u00b7 '+fc(t)}
|
|
756
|
+
var rl=document.getElementById('range-lbl');if(rl)rl.textContent=RL[r]||'All time';
|
|
757
|
+
CARD.range=RL[r]||'All time';
|
|
758
|
+
var cr=document.getElementById('card-range');if(cr)cr.textContent=CARD.range;
|
|
759
|
+
if(f.length){
|
|
760
|
+
var t=f.reduce(function(s,x){return s+x.cost},0),a=f.filter(function(x){return x.cost>0}).length;
|
|
761
|
+
CARD.cost=fc(t);CARD.days=a.toString();
|
|
762
|
+
// Update HTML card
|
|
763
|
+
var hc=document.getElementById('h-cost');if(hc)hc.textContent=fc(t);
|
|
764
|
+
var hd=document.getElementById('h-days');if(hd)hd.textContent=a;
|
|
765
|
+
// Update overview
|
|
766
|
+
var ot=document.getElementById('ov-total'),oa=document.getElementById('ov-avg');
|
|
767
|
+
if(ot)ot.textContent=fc(t);if(oa&&a>0)oa.textContent=fc(t/a)+' avg/day';
|
|
768
|
+
}
|
|
769
|
+
// Update filter button states - Stitch style
|
|
770
|
+
document.querySelectorAll('.cfilt').forEach(function(b){
|
|
771
|
+
if(b.dataset.r===r){
|
|
772
|
+
b.style.background='#c0c1ff';b.style.color='#1000a9';
|
|
773
|
+
} else {
|
|
774
|
+
b.style.background='transparent';b.style.color='#908fa0';
|
|
775
|
+
}
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
document.querySelectorAll('.cfilt').forEach(function(b){b.addEventListener('click',function(){setR(b.dataset.r)})});
|
|
780
|
+
|
|
781
|
+
// Export helpers
|
|
782
|
+
var toast=document.getElementById('toast');
|
|
783
|
+
function showToast(m){if(!toast)return;toast.textContent=m;toast.classList.add('on');setTimeout(function(){toast.classList.remove('on')},2000)}
|
|
784
|
+
|
|
785
|
+
// ─── CANVAS SHARE CARD ─────────────────────────────
|
|
786
|
+
// Renders entirely on canvas — same output for display AND video export
|
|
787
|
+
var cardCanvas=document.getElementById('share-card');
|
|
788
|
+
var cardW=1480,cardH=580,cardR=44; // 2x resolution for retina
|
|
789
|
+
cardCanvas.width=cardW;cardCanvas.height=cardH;
|
|
790
|
+
var cardCtx=cardCanvas.getContext('2d');
|
|
791
|
+
|
|
792
|
+
function roundRect(ctx,x,y,w,h,r){
|
|
793
|
+
ctx.beginPath();ctx.moveTo(x+r,y);ctx.lineTo(x+w-r,y);ctx.quadraticCurveTo(x+w,y,x+w,y+r);
|
|
794
|
+
ctx.lineTo(x+w,y+h-r);ctx.quadraticCurveTo(x+w,y+h,x+w-r,y+h);ctx.lineTo(x+r,y+h);
|
|
795
|
+
ctx.quadraticCurveTo(x,y+h,x,y+h-r);ctx.lineTo(x,y+r);ctx.quadraticCurveTo(x,y,x+r,y);ctx.closePath();
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
function drawCard(ctx,w,h,shimmerT){
|
|
799
|
+
// Background gradient
|
|
800
|
+
var bg=ctx.createLinearGradient(0,0,w,h);
|
|
801
|
+
bg.addColorStop(0,'#1a1b2e');bg.addColorStop(0.4,'#0f1018');bg.addColorStop(0.7,'#191a2d');bg.addColorStop(1,'#12131f');
|
|
802
|
+
roundRect(ctx,0,0,w,h,cardR);ctx.save();ctx.clip();
|
|
803
|
+
ctx.fillStyle=bg;ctx.fillRect(0,0,w,h);
|
|
804
|
+
|
|
805
|
+
var pad=80,padT=72;
|
|
806
|
+
|
|
807
|
+
// Grade badge
|
|
808
|
+
var bx=pad,by=padT,bs=112;
|
|
809
|
+
roundRect(ctx,bx,by,bs,bs,24);
|
|
810
|
+
ctx.fillStyle=CARD.gradeColor;ctx.fill();
|
|
811
|
+
ctx.font='800 56px "JetBrains Mono",monospace';ctx.fillStyle='#0f1018';ctx.textAlign='center';ctx.textBaseline='middle';
|
|
812
|
+
ctx.fillText(CARD.grade,bx+bs/2,by+bs/2);
|
|
813
|
+
|
|
814
|
+
// Grade label + performance text
|
|
815
|
+
ctx.textAlign='left';ctx.textBaseline='top';
|
|
816
|
+
ctx.font='700 18px "JetBrains Mono",monospace';ctx.fillStyle=CARD.gradeColor;
|
|
817
|
+
ctx.fillText(CARD.gradeLabel.toUpperCase(),bx+bs+32,by+12);
|
|
818
|
+
ctx.font='700 36px "Inter",sans-serif';ctx.fillStyle='#e3e2e3';
|
|
819
|
+
ctx.fillText(CARD.gradePerf,bx+bs+32,by+40);
|
|
820
|
+
|
|
821
|
+
// Top right: Claude Code + range
|
|
822
|
+
ctx.textAlign='right';
|
|
823
|
+
ctx.font='700 20px "JetBrains Mono",monospace';ctx.fillStyle='#908fa0';
|
|
824
|
+
ctx.fillText('CLAUDE CODE',w-pad,padT+16);
|
|
825
|
+
ctx.font='400 16px "JetBrains Mono",monospace';ctx.fillStyle='#464554';
|
|
826
|
+
ctx.fillText(CARD.range,w-pad,padT+44);
|
|
827
|
+
|
|
828
|
+
// Stats row
|
|
829
|
+
var statsY=h*0.45;
|
|
830
|
+
var labels=['TOTAL SPEND','ACTIVE DAYS','CACHE RATIO'];
|
|
831
|
+
var values=[CARD.cost,CARD.days,CARD.ratio];
|
|
832
|
+
var positions=[pad,w*0.38,w*0.7];
|
|
833
|
+
var aligns=['left','center','right'];
|
|
834
|
+
var xEnds=[null,null,w-pad];
|
|
835
|
+
|
|
836
|
+
for(var i=0;i<3;i++){
|
|
837
|
+
ctx.textAlign=i===2?'right':i===1?'center':'left';
|
|
838
|
+
var sx=i===2?w-pad:i===1?w/2:pad;
|
|
839
|
+
ctx.font='400 16px "Inter",sans-serif';ctx.fillStyle='#908fa0';
|
|
840
|
+
ctx.fillText(labels[i],sx,statsY);
|
|
841
|
+
ctx.font='700 64px "JetBrains Mono",monospace';ctx.fillStyle='#e3e2e3';
|
|
842
|
+
ctx.fillText(values[i],sx,statsY+28);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
// Bottom: diagnosis + branding
|
|
846
|
+
var botY=h-padT;
|
|
847
|
+
ctx.textAlign='left';
|
|
848
|
+
ctx.font='400 20px "Inter",sans-serif';ctx.fillStyle='#908fa0';
|
|
849
|
+
ctx.fillText(CARD.diagnosis,pad,botY);
|
|
850
|
+
|
|
851
|
+
// Branding — measure text to space properly
|
|
852
|
+
ctx.textAlign='right';
|
|
853
|
+
ctx.font='500 18px "JetBrains Mono",monospace';
|
|
854
|
+
var moverW=ctx.measureText('Mover OS').width;
|
|
855
|
+
ctx.font='400 18px "Inter",sans-serif';
|
|
856
|
+
var shippedW=ctx.measureText(' shipped fast with ').width;
|
|
857
|
+
ctx.font='500 18px "JetBrains Mono",monospace';
|
|
858
|
+
var hubberW=ctx.measureText('CC Hubber').width;
|
|
859
|
+
|
|
860
|
+
var bx=w-pad;
|
|
861
|
+
ctx.font='500 18px "JetBrains Mono",monospace';ctx.fillStyle='#c0c1ff';
|
|
862
|
+
ctx.fillText('Mover OS',bx,botY);
|
|
863
|
+
bx-=moverW;
|
|
864
|
+
ctx.font='400 18px "Inter",sans-serif';ctx.fillStyle='#908fa0';
|
|
865
|
+
ctx.fillText(' shipped fast with ',bx,botY);
|
|
866
|
+
bx-=shippedW;
|
|
867
|
+
ctx.font='500 18px "JetBrains Mono",monospace';ctx.fillStyle='#c0c1ff';
|
|
868
|
+
ctx.fillText('CC Hubber',bx,botY);
|
|
869
|
+
|
|
870
|
+
// Subtle shimmer sweep
|
|
871
|
+
if(shimmerT!==undefined){
|
|
872
|
+
var sx=-w*0.4+(shimmerT%1)*w*1.8;
|
|
873
|
+
var sg=ctx.createLinearGradient(sx,0,sx+w*0.3,h);
|
|
874
|
+
sg.addColorStop(0,'rgba(255,255,255,0)');
|
|
875
|
+
sg.addColorStop(0.45,'rgba(192,193,255,0.02)');
|
|
876
|
+
sg.addColorStop(0.5,'rgba(255,255,255,0.045)');
|
|
877
|
+
sg.addColorStop(0.55,'rgba(212,187,255,0.02)');
|
|
878
|
+
sg.addColorStop(1,'rgba(255,255,255,0)');
|
|
879
|
+
ctx.fillStyle=sg;ctx.fillRect(0,0,w,h);
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
ctx.restore();
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// Animate the card on the page
|
|
886
|
+
var cardAnimStart=null;
|
|
887
|
+
function animateCard(ts){
|
|
888
|
+
if(!cardAnimStart)cardAnimStart=ts;
|
|
889
|
+
var t=((ts-cardAnimStart)%6000)/6000;
|
|
890
|
+
drawCard(cardCtx,cardW,cardH,t);
|
|
891
|
+
requestAnimationFrame(animateCard);
|
|
892
|
+
}
|
|
893
|
+
document.fonts.ready.then(function(){requestAnimationFrame(animateCard)});
|
|
894
|
+
|
|
895
|
+
// ─── VIDEO EXPORT (records the same canvas) ──────
|
|
896
|
+
// Video export — captures HTML card with html-to-image, animates on canvas at 1440p
|
|
897
|
+
var gb=document.getElementById('btn-gif');
|
|
898
|
+
if(gb)gb.addEventListener('click',function(){
|
|
899
|
+
gb.textContent='Capturing...';gb.disabled=true;
|
|
900
|
+
var htmlCard=document.getElementById('share-card-html');
|
|
901
|
+
if(!htmlCard||typeof htmlToImage==='undefined'){gb.textContent='Share';gb.disabled=false;showToast('Library not loaded');return}
|
|
902
|
+
|
|
903
|
+
// Pause animation + hide CSS shimmer/noise for clean capture
|
|
904
|
+
htmlCard.style.animation='none';htmlCard.style.transform='none';
|
|
905
|
+
htmlCard.classList.add('no-shimmer');
|
|
906
|
+
|
|
907
|
+
document.fonts.ready.then(function(){
|
|
908
|
+
return htmlToImage.toPng(htmlCard,{quality:1,pixelRatio:3}).then(function(dataUrl){
|
|
909
|
+
htmlCard.style.animation='';htmlCard.style.transform='';
|
|
910
|
+
htmlCard.classList.remove('no-shimmer');
|
|
911
|
+
gb.textContent='Recording...';
|
|
912
|
+
|
|
913
|
+
var img=new Image();
|
|
914
|
+
img.onload=function(){
|
|
915
|
+
// 2560x1440 canvas
|
|
916
|
+
var VW=2560,VH=1440;
|
|
917
|
+
var vidCanvas=document.createElement('canvas');vidCanvas.width=VW;vidCanvas.height=VH;
|
|
918
|
+
var vctx=vidCanvas.getContext('2d');
|
|
919
|
+
vctx.imageSmoothingEnabled=true;vctx.imageSmoothingQuality='high';
|
|
920
|
+
|
|
921
|
+
// Scale card to fill ~90% of frame width for maximum impact
|
|
922
|
+
var scale=Math.min((VW*0.88)/img.width,(VH*0.82)/img.height);
|
|
923
|
+
var cw=Math.round(img.width*scale),ch=Math.round(img.height*scale);
|
|
924
|
+
var r=22*3*scale; // border radius
|
|
925
|
+
|
|
926
|
+
var stream=vidCanvas.captureStream(30);
|
|
927
|
+
var chunks=[];
|
|
928
|
+
// Prefer MP4 (Chrome 124+, works on X/Twitter), fallback WebM
|
|
929
|
+
var mime=MediaRecorder.isTypeSupported('video/mp4;codecs=avc1')?'video/mp4;codecs=avc1'
|
|
930
|
+
:MediaRecorder.isTypeSupported('video/mp4')?'video/mp4':'video/webm';
|
|
931
|
+
var ext=mime.startsWith('video/mp4')?'mp4':'webm';
|
|
932
|
+
var recorder=new MediaRecorder(stream,{mimeType:mime,videoBitsPerSecond:30000000});
|
|
933
|
+
recorder.ondataavailable=function(e){if(e.data.size>0)chunks.push(e.data)};
|
|
934
|
+
recorder.onstop=function(){
|
|
935
|
+
var blob=new Blob(chunks,{type:mime.split(';')[0]});
|
|
936
|
+
var a=document.createElement('a');a.download='cchubber-card.'+ext;
|
|
937
|
+
a.href=URL.createObjectURL(blob);a.click();
|
|
938
|
+
gb.innerHTML='<span class="material-symbols-outlined text-sm">share</span> Share';
|
|
939
|
+
gb.disabled=false;showToast('Video saved ('+ext.toUpperCase()+')');
|
|
940
|
+
};
|
|
941
|
+
|
|
942
|
+
// Pre-generate noise texture to fight gradient banding
|
|
943
|
+
var noiseC=document.createElement('canvas');noiseC.width=256;noiseC.height=256;
|
|
944
|
+
var nctx=noiseC.getContext('2d');
|
|
945
|
+
var ndata=nctx.createImageData(256,256);
|
|
946
|
+
for(var ni=0;ni<ndata.data.length;ni+=4){var v=Math.random()*255;ndata.data[ni]=v;ndata.data[ni+1]=v;ndata.data[ni+2]=v;ndata.data[ni+3]=8;}
|
|
947
|
+
nctx.putImageData(ndata,0,0);
|
|
948
|
+
|
|
949
|
+
var duration=6000,startTime=null;
|
|
950
|
+
recorder.start(100);
|
|
951
|
+
|
|
952
|
+
function frame(ts){
|
|
953
|
+
if(!startTime)startTime=ts;
|
|
954
|
+
var elapsed=ts-startTime;
|
|
955
|
+
if(elapsed>=duration){setTimeout(function(){recorder.stop()},300);return}
|
|
956
|
+
|
|
957
|
+
var t=elapsed/duration;
|
|
958
|
+
// Gentle breathe + float — matches the natural feel of the CSS animation
|
|
959
|
+
// ease-in-out via cosine (same curve as CSS ease-in-out)
|
|
960
|
+
// Gentle lateral drift — subtle, no shake
|
|
961
|
+
var drift=Math.sin(t*Math.PI*2)*5;
|
|
962
|
+
|
|
963
|
+
vctx.fillStyle='#000';vctx.fillRect(0,0,VW,VH);
|
|
964
|
+
vctx.save();
|
|
965
|
+
vctx.translate((VW-cw)/2+drift,(VH-ch)/2);
|
|
966
|
+
|
|
967
|
+
// Draw the HTML-captured card image (browser-quality)
|
|
968
|
+
vctx.drawImage(img,0,0,cw,ch);
|
|
969
|
+
|
|
970
|
+
// Clip shimmer + noise to rounded card shape (prevents corner bleed)
|
|
971
|
+
var cr=22*3*scale; // 22px radius * 3 pixelRatio * video scale
|
|
972
|
+
vctx.beginPath();
|
|
973
|
+
vctx.moveTo(cr,0);vctx.lineTo(cw-cr,0);vctx.quadraticCurveTo(cw,0,cw,cr);
|
|
974
|
+
vctx.lineTo(cw,ch-cr);vctx.quadraticCurveTo(cw,ch,cw-cr,ch);vctx.lineTo(cr,ch);
|
|
975
|
+
vctx.quadraticCurveTo(0,ch,0,ch-cr);vctx.lineTo(0,cr);vctx.quadraticCurveTo(0,0,cr,0);
|
|
976
|
+
vctx.closePath();vctx.clip();
|
|
977
|
+
|
|
978
|
+
// Shimmer sweep
|
|
979
|
+
var shimProgress=(t*2)%1;
|
|
980
|
+
var sx=-cw*0.5+shimProgress*cw*2;
|
|
981
|
+
var g=vctx.createLinearGradient(sx,0,sx+cw*0.4,ch);
|
|
982
|
+
g.addColorStop(0,'rgba(255,255,255,0)');
|
|
983
|
+
g.addColorStop(0.3,'rgba(192,193,255,0.04)');
|
|
984
|
+
g.addColorStop(0.45,'rgba(212,187,255,0.06)');
|
|
985
|
+
g.addColorStop(0.55,'rgba(192,193,255,0.06)');
|
|
986
|
+
g.addColorStop(0.7,'rgba(212,187,255,0.04)');
|
|
987
|
+
g.addColorStop(1,'rgba(255,255,255,0)');
|
|
988
|
+
vctx.fillStyle=g;vctx.fillRect(0,0,cw,ch);
|
|
989
|
+
|
|
990
|
+
// Noise dither
|
|
991
|
+
var pat=vctx.createPattern(noiseC,'repeat');
|
|
992
|
+
vctx.fillStyle=pat;vctx.globalAlpha=0.4;vctx.fillRect(0,0,cw,ch);vctx.globalAlpha=1;
|
|
993
|
+
|
|
994
|
+
vctx.restore();
|
|
995
|
+
requestAnimationFrame(frame);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
requestAnimationFrame(frame);
|
|
999
|
+
};
|
|
1000
|
+
img.src=dataUrl;
|
|
1001
|
+
});
|
|
1002
|
+
}).catch(function(e){
|
|
1003
|
+
htmlCard.style.animation='';htmlCard.style.transform='';htmlCard.classList.remove('no-shimmer');
|
|
1004
|
+
gb.innerHTML='<span class="material-symbols-outlined text-sm">share</span> Share';
|
|
1005
|
+
gb.disabled=false;showToast('Failed: '+e.message);
|
|
1006
|
+
});
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
setR('all');
|
|
1010
|
+
})();
|
|
1011
|
+
</script>
|
|
1012
|
+
</body>
|
|
1013
|
+
</html>`;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
export function renderRateLimits(usage) {
|
|
1017
|
+
const fiveHour = usage.five_hour;
|
|
1018
|
+
const sevenDay = usage.seven_day;
|
|
1019
|
+
if (!fiveHour && !sevenDay) return '';
|
|
1020
|
+
|
|
1021
|
+
const fivePct = fiveHour?.utilization ?? 0;
|
|
1022
|
+
const sevenPct = sevenDay?.utilization ?? 0;
|
|
1023
|
+
const fiveColor = fivePct > 80 ? '#ffb4ab' : fivePct > 50 ? '#ffb690' : '#c0c1ff';
|
|
1024
|
+
const sevenColor = sevenPct > 80 ? '#ffb4ab' : sevenPct > 50 ? '#ffb690' : '#c0c1ff';
|
|
1025
|
+
|
|
1026
|
+
return `
|
|
1027
|
+
<section class="grid grid-cols-1 md:grid-cols-2 rounded-xl overflow-hidden border border-[rgba(70,69,84,0.15)]" style="gap:1px; background:rgba(70,69,84,0.15);">
|
|
1028
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
1029
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">5-Hour Session</span>
|
|
1030
|
+
<span class="font-mono text-2xl font-bold block" style="color:${fiveColor}">${fivePct}%</span>
|
|
1031
|
+
<div class="h-1.5 bg-[#343536] rounded-full overflow-hidden mt-3 mb-2">
|
|
1032
|
+
<div class="h-full rounded-full" style="width:${fivePct}%;background:${fiveColor}"></div>
|
|
1033
|
+
</div>
|
|
1034
|
+
<span class="text-[10px] text-[#908fa0] block font-mono">${fiveHour?.resets_at ? 'Resets ' + new Date(fiveHour.resets_at).toLocaleTimeString() : ''}</span>
|
|
1035
|
+
</div>
|
|
1036
|
+
<div class="p-6 bg-[#0d0e0f]">
|
|
1037
|
+
<span class="text-[10px] uppercase tracking-[0.05em] text-[#908fa0] block mb-3">7-Day Rolling</span>
|
|
1038
|
+
<span class="font-mono text-2xl font-bold block" style="color:${sevenColor}">${sevenPct}%</span>
|
|
1039
|
+
<div class="h-1.5 bg-[#343536] rounded-full overflow-hidden mt-3 mb-2">
|
|
1040
|
+
<div class="h-full rounded-full" style="width:${sevenPct}%;background:${sevenColor}"></div>
|
|
1041
|
+
</div>
|
|
1042
|
+
<span class="text-[10px] text-[#908fa0] block font-mono">${sevenDay?.resets_at ? 'Resets ' + new Date(sevenDay.resets_at).toLocaleDateString() : ''}</span>
|
|
1043
|
+
</div>
|
|
1044
|
+
</section>`;
|
|
1045
|
+
}
|