agent-usage-report 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.
@@ -0,0 +1,1837 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta
6
+ name="viewport"
7
+ content="width=device-width, initial-scale=1, viewport-fit=cover"
8
+ />
9
+ <title>Agent Usage Report</title>
10
+ <style>
11
+ :root {
12
+ --bg: #ffffff;
13
+ --panel: #ffffff;
14
+ --panel-strong: #ffffff;
15
+ --ink: #111111;
16
+ --muted: #888888;
17
+ --border: #e8e8e8;
18
+ --shadow: none;
19
+ --accent: #111111;
20
+ --accent-soft: rgba(0, 0, 0, 0.04);
21
+ --cell-size: 13px;
22
+ --cell-gap: 2px;
23
+ --cell-0: #eaeaea;
24
+ --cell-1: #dbeafe;
25
+ --cell-2: #93c5fd;
26
+ --cell-3: #3b82f6;
27
+ --cell-4: #1d4ed8;
28
+ --cell-5: #1e3a5f;
29
+ --legend-text: #bbbbbb;
30
+ }
31
+
32
+ * {
33
+ box-sizing: border-box;
34
+ }
35
+
36
+ html,
37
+ body {
38
+ margin: 0;
39
+ min-height: 100%;
40
+ }
41
+
42
+ body {
43
+ background: var(--bg);
44
+ color: var(--ink);
45
+ font-family:
46
+ "Inter",
47
+ "SF Pro Display",
48
+ "Segoe UI",
49
+ system-ui,
50
+ sans-serif;
51
+ }
52
+
53
+ .page {
54
+ max-width: 1480px;
55
+ margin: 0 auto;
56
+ padding: 40px 24px 56px;
57
+ }
58
+
59
+ .shell {
60
+ background: transparent;
61
+ border: 0;
62
+ border-radius: 0;
63
+ padding: 0;
64
+ }
65
+
66
+ .header {
67
+ display: flex;
68
+ justify-content: space-between;
69
+ align-items: flex-start;
70
+ gap: 24px;
71
+ }
72
+
73
+ .header-copy {
74
+ max-width: 560px;
75
+ }
76
+
77
+ .eyebrow {
78
+ display: none;
79
+ }
80
+
81
+ .title {
82
+ margin: 0;
83
+ font-size: 20px;
84
+ line-height: 1.2;
85
+ letter-spacing: -0.01em;
86
+ font-weight: 500;
87
+ }
88
+
89
+ .subtitle {
90
+ margin: 6px 0 0;
91
+ font-size: 13px;
92
+ line-height: 1.5;
93
+ color: var(--muted);
94
+ }
95
+
96
+ .totals {
97
+ display: grid;
98
+ grid-template-columns: repeat(3, minmax(160px, 1fr));
99
+ gap: 16px;
100
+ min-width: min(100%, 560px);
101
+ }
102
+
103
+ .total-card {
104
+ text-align: right;
105
+ padding: 10px 0;
106
+ }
107
+
108
+ .total-label {
109
+ margin: 0;
110
+ font-size: 11px;
111
+ font-weight: 500;
112
+ letter-spacing: 0.03em;
113
+ text-transform: uppercase;
114
+ color: #999999;
115
+ }
116
+
117
+ .total-value {
118
+ margin: 4px 0 0;
119
+ font-size: 18px;
120
+ line-height: 1.2;
121
+ letter-spacing: -0.01em;
122
+ font-weight: 500;
123
+ }
124
+
125
+ .toolbar {
126
+ margin-top: 16px;
127
+ display: flex;
128
+ flex-direction: column;
129
+ gap: 12px;
130
+ }
131
+
132
+ .toolbar-row {
133
+ display: flex;
134
+ justify-content: space-between;
135
+ align-items: center;
136
+ gap: 12px;
137
+ flex-wrap: wrap;
138
+ }
139
+
140
+ .segmented {
141
+ display: inline-flex;
142
+ gap: 2px;
143
+ padding: 3px;
144
+ border-radius: 6px;
145
+ background: #f3f4f6;
146
+ border: 1px solid var(--border);
147
+ }
148
+
149
+ .segmented[hidden] {
150
+ display: none;
151
+ }
152
+
153
+ .segmented button {
154
+ appearance: none;
155
+ border: 0;
156
+ background: transparent;
157
+ color: var(--muted);
158
+ border-radius: 4px;
159
+ padding: 5px 12px;
160
+ font: inherit;
161
+ font-size: 12px;
162
+ font-weight: 500;
163
+ cursor: pointer;
164
+ transition:
165
+ background 120ms ease,
166
+ color 120ms ease;
167
+ }
168
+
169
+ .segmented button:hover {
170
+ color: var(--ink);
171
+ }
172
+
173
+ .segmented button.is-active {
174
+ background: var(--ink);
175
+ color: #ffffff;
176
+ }
177
+
178
+ #provider-controls {
179
+ background: transparent;
180
+ border: 0;
181
+ border-bottom: 1px solid var(--border);
182
+ border-radius: 0;
183
+ padding: 0;
184
+ gap: 0;
185
+ }
186
+
187
+ #provider-controls button {
188
+ border-radius: 0;
189
+ padding: 8px 16px;
190
+ font-size: 13px;
191
+ font-weight: 400;
192
+ color: var(--muted);
193
+ border-bottom: 2px solid transparent;
194
+ margin-bottom: -1px;
195
+ }
196
+
197
+ #provider-controls button:hover {
198
+ color: var(--ink);
199
+ }
200
+
201
+ #provider-controls button.is-active {
202
+ background: transparent;
203
+ color: var(--ink);
204
+ font-weight: 500;
205
+ border-bottom-color: var(--ink);
206
+ }
207
+
208
+ .meta-note {
209
+ font-size: 12px;
210
+ color: #aaa;
211
+ }
212
+
213
+ .meta-inline {
214
+ display: inline-flex;
215
+ align-items: center;
216
+ gap: 8px;
217
+ flex-wrap: wrap;
218
+ }
219
+
220
+ .meta-badge {
221
+ appearance: none;
222
+ border: 1px solid var(--border);
223
+ background: #f9fafb;
224
+ color: var(--ink);
225
+ border-radius: 4px;
226
+ padding: 3px 8px;
227
+ font: inherit;
228
+ font-size: 11px;
229
+ line-height: 1;
230
+ cursor: help;
231
+ transition:
232
+ background 120ms ease,
233
+ border-color 120ms ease;
234
+ }
235
+
236
+ .meta-badge:hover {
237
+ background: #f0f0f0;
238
+ border-color: #ccc;
239
+ }
240
+
241
+ .meta-tooltip-wrap {
242
+ position: relative;
243
+ display: inline-flex;
244
+ align-items: center;
245
+ }
246
+
247
+ .meta-hovercard {
248
+ position: absolute;
249
+ bottom: calc(100% + 8px);
250
+ left: 50%;
251
+ transform: translateX(-50%);
252
+ z-index: 25;
253
+ min-width: 220px;
254
+ max-width: 320px;
255
+ padding: 10px 12px;
256
+ border-radius: 8px;
257
+ background: rgba(15, 23, 42, 0.96);
258
+ color: #f8fafc;
259
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
260
+ font-size: 12px;
261
+ line-height: 1.5;
262
+ display: none;
263
+ white-space: normal;
264
+ }
265
+
266
+ .meta-hovercard strong {
267
+ color: #ffffff;
268
+ display: block;
269
+ margin-bottom: 6px;
270
+ font-weight: 600;
271
+ }
272
+
273
+ .meta-hovercard ul {
274
+ margin: 0;
275
+ padding-left: 16px;
276
+ }
277
+
278
+ .meta-hovercard li {
279
+ margin: 2px 0;
280
+ }
281
+
282
+ .meta-tooltip-wrap:hover .meta-hovercard,
283
+ .meta-tooltip-wrap:focus-within .meta-hovercard {
284
+ display: block;
285
+ }
286
+
287
+ .heatmap-panel {
288
+ margin-top: 22px;
289
+ padding: 24px 0 18px;
290
+ }
291
+
292
+ .heatmap-scroll {
293
+ overflow-x: auto;
294
+ padding-bottom: 10px;
295
+ }
296
+
297
+ .heatmap-frame {
298
+ display: grid;
299
+ grid-template-columns: 52px auto;
300
+ gap: 10px;
301
+ min-width: max-content;
302
+ }
303
+
304
+ .weekday-column {
305
+ padding-top: 28px;
306
+ display: grid;
307
+ grid-template-rows: repeat(7, var(--cell-size));
308
+ row-gap: var(--cell-gap);
309
+ align-items: center;
310
+ color: var(--legend-text);
311
+ font-size: 11px;
312
+ font-weight: 400;
313
+ }
314
+
315
+ .weekday-spacer {
316
+ visibility: hidden;
317
+ }
318
+
319
+ .grid-stack {
320
+ position: relative;
321
+ min-width: max-content;
322
+ }
323
+
324
+ .months-row {
325
+ position: relative;
326
+ height: 22px;
327
+ margin-left: 2px;
328
+ color: var(--legend-text);
329
+ font-size: 11px;
330
+ font-weight: 400;
331
+ }
332
+
333
+ .month-label {
334
+ position: absolute;
335
+ top: 0;
336
+ white-space: nowrap;
337
+ }
338
+
339
+ .heatmap-grid {
340
+ display: grid;
341
+ grid-auto-flow: column;
342
+ grid-auto-columns: var(--cell-size);
343
+ grid-template-rows: repeat(7, var(--cell-size));
344
+ gap: var(--cell-gap);
345
+ }
346
+
347
+ .day-cell {
348
+ width: var(--cell-size);
349
+ height: var(--cell-size);
350
+ border-radius: 2px;
351
+ border: 0;
352
+ background: var(--cell-0);
353
+ position: relative;
354
+ transition:
355
+ transform 120ms ease,
356
+ box-shadow 120ms ease;
357
+ }
358
+
359
+ .day-cell.is-padding {
360
+ opacity: 1;
361
+ background: var(--cell-0);
362
+ }
363
+
364
+ .day-cell[data-level="0"] {
365
+ background: var(--cell-0);
366
+ }
367
+
368
+ .day-cell[data-level="1"] {
369
+ background: var(--cell-1);
370
+ }
371
+
372
+ .day-cell[data-level="2"] {
373
+ background: var(--cell-2);
374
+ }
375
+
376
+ .day-cell[data-level="3"] {
377
+ background: var(--cell-3);
378
+ }
379
+
380
+ .day-cell[data-level="4"] {
381
+ background: var(--cell-4);
382
+ }
383
+
384
+ .day-cell[data-level="5"] {
385
+ background: var(--cell-5);
386
+ }
387
+
388
+ .day-cell:not(.is-padding):hover {
389
+ transform: translateY(-1px);
390
+ box-shadow: 0 4px 10px rgba(0, 0, 0, 0.12);
391
+ }
392
+
393
+ .legend {
394
+ margin-top: 14px;
395
+ display: flex;
396
+ align-items: center;
397
+ gap: 6px;
398
+ color: var(--legend-text);
399
+ font-size: 10px;
400
+ font-weight: 400;
401
+ }
402
+
403
+ .legend-scale {
404
+ display: inline-flex;
405
+ gap: 3px;
406
+ }
407
+
408
+ .legend-cell {
409
+ width: 13px;
410
+ height: 13px;
411
+ border-radius: 2px;
412
+ border: 0;
413
+ }
414
+
415
+ .cost-table {
416
+ width: 100%;
417
+ min-width: 820px;
418
+ border-collapse: collapse;
419
+ font-size: 13px;
420
+ }
421
+
422
+ .cost-table th,
423
+ .cost-table td {
424
+ padding: 10px 12px;
425
+ border-bottom: 1px solid var(--border);
426
+ text-align: left;
427
+ white-space: nowrap;
428
+ }
429
+
430
+ .cost-table th {
431
+ font-size: 11px;
432
+ font-weight: 500;
433
+ letter-spacing: 0.03em;
434
+ text-transform: uppercase;
435
+ color: #999999;
436
+ }
437
+
438
+ .cost-table tbody tr:hover {
439
+ background: rgba(0, 0, 0, 0.02);
440
+ }
441
+
442
+ .cost-table tfoot td {
443
+ font-weight: 600;
444
+ border-top: 1px solid var(--border);
445
+ border-bottom: 0;
446
+ }
447
+
448
+ .section-header {
449
+ display: flex;
450
+ justify-content: space-between;
451
+ align-items: flex-end;
452
+ gap: 16px;
453
+ flex-wrap: wrap;
454
+ margin-bottom: 14px;
455
+ }
456
+
457
+ .monthly-comparison-stats {
458
+ display: grid;
459
+ grid-template-columns: repeat(3, minmax(0, 1fr));
460
+ gap: 18px;
461
+ margin-bottom: 18px;
462
+ }
463
+
464
+ .plan-input-wrap {
465
+ display: inline-flex;
466
+ align-items: center;
467
+ gap: 10px;
468
+ color: var(--muted);
469
+ font-size: 13px;
470
+ }
471
+
472
+ .plan-input-label {
473
+ font-size: 11px;
474
+ font-weight: 500;
475
+ letter-spacing: 0.03em;
476
+ text-transform: uppercase;
477
+ color: #999999;
478
+ }
479
+
480
+ .plan-input-shell {
481
+ display: inline-flex;
482
+ align-items: center;
483
+ gap: 6px;
484
+ padding: 6px 10px;
485
+ border-radius: 6px;
486
+ border: 1px solid var(--border);
487
+ background: #ffffff;
488
+ }
489
+
490
+ .plan-input-prefix {
491
+ color: var(--muted);
492
+ font-size: 14px;
493
+ font-weight: 500;
494
+ }
495
+
496
+ .plan-input {
497
+ width: 92px;
498
+ border: 0;
499
+ outline: 0;
500
+ background: transparent;
501
+ color: var(--ink);
502
+ font: inherit;
503
+ font-size: 14px;
504
+ font-weight: 500;
505
+ }
506
+
507
+ .delta-over {
508
+ color: #027a48;
509
+ }
510
+
511
+ .delta-under {
512
+ color: #b42318;
513
+ }
514
+
515
+ .stats {
516
+ margin-top: 20px;
517
+ padding: 0;
518
+ display: flex;
519
+ border: 1px solid var(--border);
520
+ border-radius: 8px;
521
+ background: #ffffff;
522
+ overflow: hidden;
523
+ }
524
+
525
+ .stat-card {
526
+ flex: 1;
527
+ padding: 14px 16px;
528
+ border-right: 1px solid #f0f0f0;
529
+ }
530
+
531
+ .stat-card:last-child {
532
+ border-right: 0;
533
+ }
534
+
535
+ .stat-label {
536
+ margin: 0;
537
+ color: #999999;
538
+ font-size: 11px;
539
+ font-weight: 500;
540
+ letter-spacing: 0.03em;
541
+ text-transform: uppercase;
542
+ }
543
+
544
+ .stat-value {
545
+ margin: 4px 0 0;
546
+ font-size: 18px;
547
+ line-height: 1.2;
548
+ letter-spacing: -0.01em;
549
+ font-weight: 500;
550
+ overflow-wrap: anywhere;
551
+ }
552
+
553
+ .stat-subvalue {
554
+ margin-top: 4px;
555
+ font-size: 12px;
556
+ color: var(--muted);
557
+ }
558
+
559
+ .footer {
560
+ margin-top: 18px;
561
+ padding-top: 16px;
562
+ border-top: 1px solid var(--border);
563
+ display: flex;
564
+ justify-content: space-between;
565
+ gap: 16px;
566
+ flex-wrap: wrap;
567
+ font-size: 13px;
568
+ color: var(--muted);
569
+ }
570
+
571
+ .tooltip {
572
+ position: fixed;
573
+ inset: auto auto 0 0;
574
+ transform: translate(-999px, -999px);
575
+ pointer-events: none;
576
+ min-width: 220px;
577
+ max-width: 300px;
578
+ padding: 12px 12px 10px;
579
+ border-radius: 8px;
580
+ background: rgba(15, 23, 42, 0.96);
581
+ color: #f8fafc;
582
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
583
+ transition: opacity 90ms ease;
584
+ opacity: 0;
585
+ z-index: 20;
586
+ }
587
+
588
+ .tooltip.is-visible {
589
+ opacity: 1;
590
+ }
591
+
592
+ .tooltip-title {
593
+ margin: 0 0 8px;
594
+ font-size: 14px;
595
+ font-weight: 600;
596
+ }
597
+
598
+ .tooltip-line {
599
+ margin: 4px 0;
600
+ font-size: 13px;
601
+ line-height: 1.45;
602
+ color: rgba(248, 250, 252, 0.88);
603
+ }
604
+
605
+ .tooltip-line strong {
606
+ color: #ffffff;
607
+ }
608
+
609
+ @media (max-width: 1080px) {
610
+ .header {
611
+ flex-direction: column;
612
+ }
613
+
614
+ .totals {
615
+ width: 100%;
616
+ grid-template-columns: repeat(3, minmax(0, 1fr));
617
+ }
618
+
619
+ .total-card {
620
+ text-align: left;
621
+ }
622
+
623
+ .stats {
624
+ flex-wrap: wrap;
625
+ }
626
+
627
+ .stat-card {
628
+ flex: 1 1 calc(50% - 1px);
629
+ border-bottom: 1px solid #f0f0f0;
630
+ }
631
+
632
+ .monthly-comparison-stats {
633
+ grid-template-columns: repeat(2, minmax(0, 1fr));
634
+ }
635
+ }
636
+
637
+ @media (max-width: 720px) {
638
+ :root {
639
+ --cell-size: 11px;
640
+ --cell-gap: 2px;
641
+ }
642
+
643
+ .page {
644
+ padding: 16px 12px 24px;
645
+ }
646
+
647
+ .shell {
648
+ padding: 0;
649
+ }
650
+
651
+ .totals {
652
+ grid-template-columns: 1fr;
653
+ gap: 2px;
654
+ }
655
+
656
+ .toolbar {
657
+ flex-direction: column;
658
+ align-items: stretch;
659
+ }
660
+
661
+ .segmented {
662
+ width: 100%;
663
+ flex-wrap: wrap;
664
+ }
665
+
666
+ .segmented button {
667
+ flex: 1 1 auto;
668
+ }
669
+
670
+ #provider-controls {
671
+ width: 100%;
672
+ overflow-x: auto;
673
+ }
674
+
675
+ .heatmap-panel {
676
+ padding: 14px 0 10px;
677
+ }
678
+
679
+ .heatmap-frame {
680
+ grid-template-columns: 36px auto;
681
+ }
682
+
683
+ .months-row {
684
+ font-size: 11px;
685
+ }
686
+
687
+ .stats {
688
+ flex-direction: column;
689
+ }
690
+
691
+ .stat-card {
692
+ border-right: 0;
693
+ border-bottom: 1px solid #f0f0f0;
694
+ }
695
+
696
+ .monthly-comparison-stats {
697
+ grid-template-columns: 1fr;
698
+ }
699
+
700
+ .legend {
701
+ flex-wrap: wrap;
702
+ }
703
+ }
704
+ </style>
705
+ </head>
706
+ <body>
707
+ <main class="page">
708
+ <section class="shell">
709
+ <header class="header">
710
+ <div class="header-copy">
711
+ <p class="eyebrow">Local Agent Usage</p>
712
+ <h1 class="title">Agent Usage</h1>
713
+ <p class="subtitle" id="subtitle">
714
+ Daily token usage extracted from local usage files.
715
+ </p>
716
+ </div>
717
+ <div class="totals">
718
+ <article class="total-card">
719
+ <p class="total-label">Input Tokens</p>
720
+ <p class="total-value" id="input-total">0</p>
721
+ </article>
722
+ <article class="total-card">
723
+ <p class="total-label">Output Tokens</p>
724
+ <p class="total-value" id="output-total">0</p>
725
+ </article>
726
+ <article class="total-card">
727
+ <p class="total-label">Total Tokens</p>
728
+ <p class="total-value" id="token-total">0</p>
729
+ </article>
730
+ </div>
731
+ </header>
732
+
733
+ <div class="toolbar">
734
+ <div class="segmented" id="provider-controls" hidden></div>
735
+ <div class="toolbar-row">
736
+ <div style="display:flex;gap:8px;align-items:center;flex-wrap:wrap;">
737
+ <div class="segmented" id="range-controls"></div>
738
+ <div class="segmented" id="metric-controls"></div>
739
+ </div>
740
+ <div class="meta-note" id="range-note"></div>
741
+ </div>
742
+ </div>
743
+
744
+ <section class="heatmap-panel">
745
+ <div class="heatmap-scroll">
746
+ <div class="heatmap-frame">
747
+ <div class="weekday-column" id="weekday-column"></div>
748
+ <div class="grid-stack">
749
+ <div class="months-row" id="months-row"></div>
750
+ <div class="heatmap-grid" id="heatmap-grid"></div>
751
+ </div>
752
+ </div>
753
+ </div>
754
+ <div class="legend">
755
+ <span>Less</span>
756
+ <div class="legend-scale" id="legend-scale"></div>
757
+ <span>More</span>
758
+ </div>
759
+ </section>
760
+
761
+ <section class="stats">
762
+ <article class="stat-card">
763
+ <p class="stat-label">Average / Day</p>
764
+ <p class="stat-value" id="average-per-day">0</p>
765
+ <p class="stat-subvalue" id="average-detail"></p>
766
+ </article>
767
+ <article class="stat-card">
768
+ <p class="stat-label">Most Used Model</p>
769
+ <p class="stat-value" id="top-model">-</p>
770
+ <p class="stat-subvalue" id="top-model-detail"></p>
771
+ </article>
772
+ <article class="stat-card">
773
+ <p class="stat-label">Recent Use (Last 30 Days)</p>
774
+ <p class="stat-value" id="recent-model">-</p>
775
+ <p class="stat-subvalue" id="recent-model-detail"></p>
776
+ </article>
777
+ <article class="stat-card">
778
+ <p class="stat-label">Longest Streak</p>
779
+ <p class="stat-value" id="longest-streak">0 days</p>
780
+ <p class="stat-subvalue" id="longest-streak-detail"></p>
781
+ </article>
782
+ <article class="stat-card">
783
+ <p class="stat-label">Current Streak</p>
784
+ <p class="stat-value" id="current-streak">0 days</p>
785
+ <p class="stat-subvalue" id="current-streak-detail"></p>
786
+ </article>
787
+ <article class="stat-card">
788
+ <p class="stat-label">Estimated Cost</p>
789
+ <p class="stat-value" id="estimated-cost">$0.00</p>
790
+ <p class="stat-subvalue" id="estimated-cost-detail"></p>
791
+ </article>
792
+ </section>
793
+
794
+ <section class="heatmap-panel">
795
+ <div class="section-header">
796
+ <div>
797
+ <p class="stat-label" style="margin:0;">Daily Cost</p>
798
+ <div class="meta-note meta-inline">
799
+ <span id="cost-table-note"></span>
800
+ <span class="meta-tooltip-wrap" id="missing-pricing-wrap" hidden>
801
+ <button type="button" class="meta-badge" id="missing-pricing-details" aria-label="Show missing pricing models">?</button>
802
+ <div class="meta-hovercard" id="missing-pricing-tooltip" role="tooltip"></div>
803
+ </span>
804
+ </div>
805
+ </div>
806
+ <div class="meta-note" id="pricing-note"></div>
807
+ </div>
808
+ <div class="heatmap-scroll">
809
+ <table class="cost-table">
810
+ <thead>
811
+ <tr>
812
+ <th>Date</th>
813
+ <th>Cost</th>
814
+ <th>Total Tokens</th>
815
+ <th>Input</th>
816
+ <th>Cached</th>
817
+ <th>Output</th>
818
+ <th>Top Model</th>
819
+ </tr>
820
+ </thead>
821
+ <tbody id="cost-table-body"></tbody>
822
+ <tfoot>
823
+ <tr>
824
+ <td>Total</td>
825
+ <td id="cost-table-total-cost">$0.00</td>
826
+ <td id="cost-table-total-tokens">0</td>
827
+ <td id="cost-table-total-input">0</td>
828
+ <td id="cost-table-total-cached">0</td>
829
+ <td id="cost-table-total-output">0</td>
830
+ <td id="cost-table-total-models">-</td>
831
+ </tr>
832
+ </tfoot>
833
+ </table>
834
+ </div>
835
+ </section>
836
+
837
+ <section class="heatmap-panel" id="monthly-comparison-section" hidden>
838
+ <div class="section-header">
839
+ <div>
840
+ <p class="stat-label" style="margin:0;" id="monthly-comparison-title">Monthly Spend vs Plan</p>
841
+ <div class="meta-note" id="monthly-comparison-note"></div>
842
+ </div>
843
+ <label class="plan-input-wrap" for="provider-plan-cost-input">
844
+ <span class="plan-input-label">Monthly Plan</span>
845
+ <span class="plan-input-shell">
846
+ <span class="plan-input-prefix">$</span>
847
+ <input
848
+ class="plan-input"
849
+ id="provider-plan-cost-input"
850
+ type="number"
851
+ min="0"
852
+ step="1"
853
+ inputmode="decimal"
854
+ />
855
+ </span>
856
+ </label>
857
+ </div>
858
+
859
+ <div class="monthly-comparison-stats">
860
+ <article class="stat-card">
861
+ <p class="stat-label">Current Month Spend</p>
862
+ <p class="stat-value" id="monthly-current-spend">$0.00</p>
863
+ <p class="stat-subvalue" id="monthly-current-spend-detail"></p>
864
+ </article>
865
+ <article class="stat-card">
866
+ <p class="stat-label">Current Month Delta</p>
867
+ <p class="stat-value" id="monthly-current-delta">$0.00</p>
868
+ <p class="stat-subvalue" id="monthly-current-delta-detail"></p>
869
+ </article>
870
+ <article class="stat-card">
871
+ <p class="stat-label">Months Over Plan</p>
872
+ <p class="stat-value" id="monthly-over-count">0</p>
873
+ <p class="stat-subvalue" id="monthly-over-count-detail"></p>
874
+ </article>
875
+ </div>
876
+
877
+ <div class="heatmap-scroll">
878
+ <table class="cost-table">
879
+ <thead>
880
+ <tr>
881
+ <th>Month</th>
882
+ <th>Spend</th>
883
+ <th>Plan</th>
884
+ <th>Delta</th>
885
+ <th>Total Tokens</th>
886
+ <th>Top Model</th>
887
+ </tr>
888
+ </thead>
889
+ <tbody id="monthly-table-body"></tbody>
890
+ <tfoot>
891
+ <tr>
892
+ <td>Total</td>
893
+ <td id="monthly-table-total-spend">$0.00</td>
894
+ <td id="monthly-table-total-plan">$0.00</td>
895
+ <td id="monthly-table-total-delta">$0.00</td>
896
+ <td id="monthly-table-total-tokens">0</td>
897
+ <td id="monthly-table-total-months">0 month(s)</td>
898
+ </tr>
899
+ </tfoot>
900
+ </table>
901
+ </div>
902
+ </section>
903
+
904
+ <footer class="footer">
905
+ <div id="scan-summary"></div>
906
+ <div id="generation-summary"></div>
907
+ </footer>
908
+ </section>
909
+ </main>
910
+
911
+ <div class="tooltip" id="tooltip"></div>
912
+
913
+ <script id="report-data" type="application/json">__DATA__</script>
914
+ <script>
915
+ const REPORT = JSON.parse(document.getElementById("report-data").textContent);
916
+
917
+ const RANGE_OPTIONS = [
918
+ { key: "90d", label: "90D", days: 90 },
919
+ { key: "180d", label: "180D", days: 180 },
920
+ { key: "1y", label: "1Y", mode: "year_to_today" },
921
+ { key: "all", label: "All", days: null },
922
+ ];
923
+ const PLAN_COST_STORAGE_PREFIX = "agent-usage-report.plan-cost";
924
+ const DEFAULT_PLAN_COSTS = {
925
+ codex: 200,
926
+ claude: 200,
927
+ };
928
+
929
+ const METRIC_OPTIONS = [
930
+ { key: "totalTokens", label: "Total" },
931
+ { key: "inputTokens", label: "Input" },
932
+ { key: "outputTokens", label: "Output" },
933
+ { key: "cachedInputTokens", label: "Cached" },
934
+ { key: "reasoningTokens", label: "Reasoning" },
935
+ ];
936
+
937
+ const weekdayNames = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"];
938
+ const monthLabel = new Intl.DateTimeFormat("en-US", { month: "short" });
939
+ const fullDayLabel = new Intl.DateTimeFormat("en-US", {
940
+ weekday: "long",
941
+ month: "short",
942
+ day: "numeric",
943
+ year: "numeric",
944
+ });
945
+ const monthValueLabel = new Intl.DateTimeFormat("en-US", {
946
+ month: "short",
947
+ year: "numeric",
948
+ });
949
+
950
+ function createLegacyProviderReport(report) {
951
+ return {
952
+ providerId: report.defaultProvider || "codex",
953
+ providerLabel: "Codex CLI",
954
+ providerShortLabel: "Codex",
955
+ sourceHome: report.sourceHome || report.codexHome || null,
956
+ days: report.days || [],
957
+ pricing: report.pricing || {},
958
+ costTotalsUSD: report.costTotalsUSD || {},
959
+ scan: report.scan || {},
960
+ };
961
+ }
962
+
963
+ const LEGACY_PROVIDER_REPORT = createLegacyProviderReport(REPORT);
964
+ const PROVIDER_REPORTS = REPORT.providers || {
965
+ [LEGACY_PROVIDER_REPORT.providerId]: LEGACY_PROVIDER_REPORT,
966
+ };
967
+ const PROVIDER_ORDER = (
968
+ REPORT.providerOrder && REPORT.providerOrder.length
969
+ ? REPORT.providerOrder
970
+ : Object.keys(PROVIDER_REPORTS)
971
+ ).filter((providerId) => Boolean(PROVIDER_REPORTS[providerId]));
972
+ const COMBINED_REPORT = REPORT.combined || LEGACY_PROVIDER_REPORT;
973
+ const PROVIDER_OPTIONS = [];
974
+ if (PROVIDER_ORDER.length > 1) {
975
+ PROVIDER_OPTIONS.push({
976
+ key: "all",
977
+ label: (COMBINED_REPORT && COMBINED_REPORT.providerShortLabel) || "All",
978
+ });
979
+ }
980
+ for (const providerId of PROVIDER_ORDER) {
981
+ const provider = PROVIDER_REPORTS[providerId];
982
+ PROVIDER_OPTIONS.push({
983
+ key: providerId,
984
+ label: provider.providerShortLabel || provider.providerLabel || providerId,
985
+ });
986
+ }
987
+
988
+ const defaultRange = "all";
989
+ const defaultProvider =
990
+ PROVIDER_OPTIONS.find((entry) => entry.key === (REPORT.defaultProvider || ""))
991
+ ?.key || PROVIDER_OPTIONS[0]?.key || "all";
992
+ const state = {
993
+ provider: defaultProvider,
994
+ range: defaultRange,
995
+ metric: "totalTokens",
996
+ providerPlanCosts: {},
997
+ };
998
+
999
+ function getActiveReport() {
1000
+ if (state.provider === "all") {
1001
+ return COMBINED_REPORT;
1002
+ }
1003
+ return PROVIDER_REPORTS[state.provider] || COMBINED_REPORT;
1004
+ }
1005
+
1006
+ function getAllKnownDays(report = getActiveReport()) {
1007
+ return (report.days || []).map((entry) => entry.date).sort();
1008
+ }
1009
+
1010
+ function getFallbackEndDate(report = getActiveReport()) {
1011
+ const knownDays = getAllKnownDays(report);
1012
+ return REPORT.generatedLocalDate || (knownDays.length ? knownDays[knownDays.length - 1] : null);
1013
+ }
1014
+
1015
+ const elements = {
1016
+ subtitle: document.getElementById("subtitle"),
1017
+ providerControls: document.getElementById("provider-controls"),
1018
+ rangeControls: document.getElementById("range-controls"),
1019
+ metricControls: document.getElementById("metric-controls"),
1020
+ rangeNote: document.getElementById("range-note"),
1021
+ weekdayColumn: document.getElementById("weekday-column"),
1022
+ monthsRow: document.getElementById("months-row"),
1023
+ heatmapGrid: document.getElementById("heatmap-grid"),
1024
+ legendScale: document.getElementById("legend-scale"),
1025
+ inputTotal: document.getElementById("input-total"),
1026
+ outputTotal: document.getElementById("output-total"),
1027
+ tokenTotal: document.getElementById("token-total"),
1028
+ averagePerDay: document.getElementById("average-per-day"),
1029
+ averageDetail: document.getElementById("average-detail"),
1030
+ topModel: document.getElementById("top-model"),
1031
+ topModelDetail: document.getElementById("top-model-detail"),
1032
+ recentModel: document.getElementById("recent-model"),
1033
+ recentModelDetail: document.getElementById("recent-model-detail"),
1034
+ longestStreak: document.getElementById("longest-streak"),
1035
+ longestStreakDetail: document.getElementById("longest-streak-detail"),
1036
+ currentStreak: document.getElementById("current-streak"),
1037
+ currentStreakDetail: document.getElementById("current-streak-detail"),
1038
+ estimatedCost: document.getElementById("estimated-cost"),
1039
+ estimatedCostDetail: document.getElementById("estimated-cost-detail"),
1040
+ pricingNote: document.getElementById("pricing-note"),
1041
+ costTableNote: document.getElementById("cost-table-note"),
1042
+ missingPricingWrap: document.getElementById("missing-pricing-wrap"),
1043
+ missingPricingDetails: document.getElementById("missing-pricing-details"),
1044
+ missingPricingTooltip: document.getElementById("missing-pricing-tooltip"),
1045
+ costTableBody: document.getElementById("cost-table-body"),
1046
+ costTableTotalCost: document.getElementById("cost-table-total-cost"),
1047
+ costTableTotalTokens: document.getElementById("cost-table-total-tokens"),
1048
+ costTableTotalInput: document.getElementById("cost-table-total-input"),
1049
+ costTableTotalCached: document.getElementById("cost-table-total-cached"),
1050
+ costTableTotalOutput: document.getElementById("cost-table-total-output"),
1051
+ costTableTotalModels: document.getElementById("cost-table-total-models"),
1052
+ monthlyComparisonSection: document.getElementById("monthly-comparison-section"),
1053
+ monthlyComparisonTitle: document.getElementById("monthly-comparison-title"),
1054
+ monthlyComparisonNote: document.getElementById("monthly-comparison-note"),
1055
+ providerPlanCostInput: document.getElementById("provider-plan-cost-input"),
1056
+ monthlyCurrentSpend: document.getElementById("monthly-current-spend"),
1057
+ monthlyCurrentSpendDetail: document.getElementById("monthly-current-spend-detail"),
1058
+ monthlyCurrentDelta: document.getElementById("monthly-current-delta"),
1059
+ monthlyCurrentDeltaDetail: document.getElementById("monthly-current-delta-detail"),
1060
+ monthlyOverCount: document.getElementById("monthly-over-count"),
1061
+ monthlyOverCountDetail: document.getElementById("monthly-over-count-detail"),
1062
+ monthlyTableBody: document.getElementById("monthly-table-body"),
1063
+ monthlyTableTotalSpend: document.getElementById("monthly-table-total-spend"),
1064
+ monthlyTableTotalPlan: document.getElementById("monthly-table-total-plan"),
1065
+ monthlyTableTotalDelta: document.getElementById("monthly-table-total-delta"),
1066
+ monthlyTableTotalTokens: document.getElementById("monthly-table-total-tokens"),
1067
+ monthlyTableTotalMonths: document.getElementById("monthly-table-total-months"),
1068
+ scanSummary: document.getElementById("scan-summary"),
1069
+ generationSummary: document.getElementById("generation-summary"),
1070
+ tooltip: document.getElementById("tooltip"),
1071
+ };
1072
+
1073
+ function getPlanCostStorageKey(providerId) {
1074
+ return `${PLAN_COST_STORAGE_PREFIX}.${providerId}`;
1075
+ }
1076
+
1077
+ function getDefaultPlanCost(providerId) {
1078
+ return DEFAULT_PLAN_COSTS[providerId] ?? 200;
1079
+ }
1080
+
1081
+ function loadPlanCost(providerId) {
1082
+ try {
1083
+ const stored = window.localStorage.getItem(getPlanCostStorageKey(providerId));
1084
+ const parsed = stored == null ? NaN : Number.parseFloat(stored);
1085
+ if (Number.isFinite(parsed) && parsed >= 0) {
1086
+ return parsed;
1087
+ }
1088
+ } catch {}
1089
+ return getDefaultPlanCost(providerId);
1090
+ }
1091
+
1092
+ function savePlanCost(providerId, value) {
1093
+ try {
1094
+ window.localStorage.setItem(getPlanCostStorageKey(providerId), String(value));
1095
+ } catch {}
1096
+ }
1097
+
1098
+ function getProviderPlanCost(providerId) {
1099
+ if (!(providerId in state.providerPlanCosts)) {
1100
+ state.providerPlanCosts[providerId] = loadPlanCost(providerId);
1101
+ }
1102
+ return state.providerPlanCosts[providerId];
1103
+ }
1104
+
1105
+ function setProviderPlanCost(providerId, value) {
1106
+ state.providerPlanCosts[providerId] = value;
1107
+ savePlanCost(providerId, value);
1108
+ }
1109
+
1110
+ function formatCompactNumber(value) {
1111
+ if (!Number.isFinite(value)) {
1112
+ return "0";
1113
+ }
1114
+ const abs = Math.abs(value);
1115
+ if (abs < 1000) {
1116
+ return Math.round(value).toString();
1117
+ }
1118
+ const units = [
1119
+ { value: 1e12, suffix: "T" },
1120
+ { value: 1e9, suffix: "B" },
1121
+ { value: 1e6, suffix: "M" },
1122
+ { value: 1e3, suffix: "K" },
1123
+ ];
1124
+ for (const unit of units) {
1125
+ if (abs >= unit.value) {
1126
+ const scaled = value / unit.value;
1127
+ const digits = Math.abs(scaled) >= 100 ? 0 : Math.abs(scaled) >= 10 ? 1 : 2;
1128
+ return `${scaled.toFixed(digits).replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1")}${unit.suffix}`;
1129
+ }
1130
+ }
1131
+ return value.toString();
1132
+ }
1133
+
1134
+ function formatNumber(value) {
1135
+ return Math.round(value).toLocaleString("en-US");
1136
+ }
1137
+
1138
+ function formatCurrency(value) {
1139
+ return new Intl.NumberFormat("en-US", {
1140
+ style: "currency",
1141
+ currency: "USD",
1142
+ minimumFractionDigits: value >= 100 ? 0 : 2,
1143
+ maximumFractionDigits: value >= 100 ? 0 : 2,
1144
+ }).format(value || 0);
1145
+ }
1146
+
1147
+ function formatDate(dateString) {
1148
+ return fullDayLabel.format(new Date(`${dateString}T12:00:00`));
1149
+ }
1150
+
1151
+ function formatMonth(monthString) {
1152
+ return monthValueLabel.format(new Date(`${monthString}-01T12:00:00`));
1153
+ }
1154
+
1155
+ function parseIsoDate(dateString) {
1156
+ return new Date(`${dateString}T12:00:00`);
1157
+ }
1158
+
1159
+ function isoDateString(dateObj) {
1160
+ return dateObj.toISOString().slice(0, 10);
1161
+ }
1162
+
1163
+ function addDays(dateObj, days) {
1164
+ const next = new Date(dateObj);
1165
+ next.setUTCDate(next.getUTCDate() + days);
1166
+ return next;
1167
+ }
1168
+
1169
+ function startOfWeek(dateObj) {
1170
+ const day = dateObj.getUTCDay();
1171
+ const mondayOffset = (day + 6) % 7;
1172
+ return addDays(dateObj, -mondayOffset);
1173
+ }
1174
+
1175
+ function endOfWeek(dateObj) {
1176
+ const day = dateObj.getUTCDay();
1177
+ const sundayOffset = day === 0 ? 0 : 7 - day;
1178
+ return addDays(dateObj, sundayOffset);
1179
+ }
1180
+
1181
+ function startOfCurrentMonthLastYear(dateObj) {
1182
+ return new Date(Date.UTC(dateObj.getUTCFullYear() - 1, dateObj.getUTCMonth(), 1, 12, 0, 0));
1183
+ }
1184
+
1185
+ function getCurrentRange() {
1186
+ const report = getActiveReport();
1187
+ const allKnownDays = getAllKnownDays(report);
1188
+ const option = RANGE_OPTIONS.find((entry) => entry.key === state.range) || RANGE_OPTIONS[0];
1189
+ const fallbackEndDate = getFallbackEndDate(report);
1190
+ const endDate = parseIsoDate(fallbackEndDate);
1191
+ const baselineStart = isoDateString(startOfCurrentMonthLastYear(endDate));
1192
+ if (!option.days || allKnownDays.length === 0) {
1193
+ if (option.key === "all") {
1194
+ return {
1195
+ start: allKnownDays.length ? (allKnownDays[0] < baselineStart ? allKnownDays[0] : baselineStart) : baselineStart,
1196
+ end: fallbackEndDate,
1197
+ label: option.label,
1198
+ };
1199
+ }
1200
+ return {
1201
+ start: allKnownDays.length ? allKnownDays[0] : fallbackEndDate,
1202
+ end: fallbackEndDate,
1203
+ label: option.label,
1204
+ };
1205
+ }
1206
+ if (option.mode === "year_to_today") {
1207
+ return {
1208
+ start: baselineStart,
1209
+ end: fallbackEndDate,
1210
+ label: option.label,
1211
+ };
1212
+ }
1213
+ const startDate = addDays(endDate, -(option.days - 1));
1214
+ return {
1215
+ start: isoDateString(startDate),
1216
+ end: fallbackEndDate,
1217
+ label: option.label,
1218
+ };
1219
+ }
1220
+
1221
+ function getVisibleDays() {
1222
+ const report = getActiveReport();
1223
+ const range = getCurrentRange();
1224
+ return (report.days || []).filter((entry) => entry.date >= range.start && entry.date <= range.end);
1225
+ }
1226
+
1227
+ function getVisibleCalendar() {
1228
+ const range = getCurrentRange();
1229
+ const start = startOfWeek(parseIsoDate(range.start));
1230
+ const end = endOfWeek(parseIsoDate(range.end));
1231
+ const dates = [];
1232
+ for (let cursor = new Date(start); cursor <= end; cursor = addDays(cursor, 1)) {
1233
+ dates.push(isoDateString(cursor));
1234
+ }
1235
+ return { start: isoDateString(start), end: isoDateString(end), dates };
1236
+ }
1237
+
1238
+ function sumField(days, field) {
1239
+ return days.reduce((total, entry) => total + (entry[field] || 0), 0);
1240
+ }
1241
+
1242
+ function isActivityEntry(entry) {
1243
+ return (entry.totalTokens || 0) > 0 || (entry.displayValue || 0) > 0;
1244
+ }
1245
+
1246
+ function getHeatmapMetricValue(entry) {
1247
+ if (!entry) {
1248
+ return 0;
1249
+ }
1250
+ if (state.metric === "totalTokens" && (entry.totalTokens || 0) <= 0) {
1251
+ return entry.displayValue || 0;
1252
+ }
1253
+ return entry[state.metric] || 0;
1254
+ }
1255
+
1256
+ function getModelTotals(days) {
1257
+ const totals = new Map();
1258
+ for (const entry of days) {
1259
+ const modelTotals = entry.modelTotals || {};
1260
+ for (const [model, value] of Object.entries(modelTotals)) {
1261
+ totals.set(model, (totals.get(model) || 0) + value);
1262
+ }
1263
+ }
1264
+ return [...totals.entries()].sort((a, b) => b[1] - a[1]);
1265
+ }
1266
+
1267
+ function getTopModel(days) {
1268
+ const sorted = getModelTotals(days);
1269
+ return sorted.length ? sorted[0] : null;
1270
+ }
1271
+
1272
+ function getLast30Days(endDateString, days) {
1273
+ const end = parseIsoDate(endDateString);
1274
+ const start = addDays(end, -29);
1275
+ const startString = isoDateString(start);
1276
+ return (days || []).filter((entry) => entry.date >= startString && entry.date <= endDateString);
1277
+ }
1278
+
1279
+ function computeStreaks() {
1280
+ const report = getActiveReport();
1281
+ if (!(report.days || []).length) {
1282
+ return {
1283
+ longestCount: 0,
1284
+ longestEnd: null,
1285
+ currentCount: 0,
1286
+ currentEnd: null,
1287
+ };
1288
+ }
1289
+
1290
+ const activeDates = new Set(
1291
+ report.days
1292
+ .filter((entry) => isActivityEntry(entry))
1293
+ .map((entry) => entry.date)
1294
+ );
1295
+
1296
+ let longestCount = 0;
1297
+ let longestEnd = null;
1298
+ let currentCount = 0;
1299
+ let currentEnd = null;
1300
+ let runCount = 0;
1301
+ let previousDate = null;
1302
+
1303
+ for (const entry of report.days) {
1304
+ if (!activeDates.has(entry.date)) {
1305
+ runCount = 0;
1306
+ previousDate = entry.date;
1307
+ continue;
1308
+ }
1309
+
1310
+ if (!previousDate) {
1311
+ runCount = 1;
1312
+ } else {
1313
+ const prev = parseIsoDate(previousDate);
1314
+ const expected = isoDateString(addDays(prev, 1));
1315
+ runCount = expected === entry.date ? runCount + 1 : 1;
1316
+ }
1317
+
1318
+ if (runCount >= longestCount) {
1319
+ longestCount = runCount;
1320
+ longestEnd = entry.date;
1321
+ }
1322
+
1323
+ currentCount = runCount;
1324
+ currentEnd = entry.date;
1325
+ previousDate = entry.date;
1326
+ }
1327
+
1328
+ const latestDay = getFallbackEndDate(report);
1329
+ if (currentEnd !== latestDay) {
1330
+ currentCount = 0;
1331
+ currentEnd = latestDay;
1332
+ }
1333
+
1334
+ return { longestCount, longestEnd, currentCount, currentEnd };
1335
+ }
1336
+
1337
+ function quantile(sortedValues, ratio) {
1338
+ if (!sortedValues.length) {
1339
+ return 0;
1340
+ }
1341
+ const index = Math.floor((sortedValues.length - 1) * ratio);
1342
+ return sortedValues[index];
1343
+ }
1344
+
1345
+ function buildThresholds(values) {
1346
+ const positive = values.filter((value) => value > 0).sort((a, b) => a - b);
1347
+ if (!positive.length) {
1348
+ return [0, 0, 0, 0];
1349
+ }
1350
+ const thresholds = [
1351
+ quantile(positive, 0.2),
1352
+ quantile(positive, 0.45),
1353
+ quantile(positive, 0.7),
1354
+ quantile(positive, 0.9),
1355
+ ].map((value) => Math.max(1, value));
1356
+ for (let index = 1; index < thresholds.length; index += 1) {
1357
+ thresholds[index] = Math.max(thresholds[index], thresholds[index - 1]);
1358
+ }
1359
+ return thresholds;
1360
+ }
1361
+
1362
+ function levelForValue(value, thresholds) {
1363
+ if (!value) {
1364
+ return 0;
1365
+ }
1366
+ if (value <= thresholds[0]) {
1367
+ return 1;
1368
+ }
1369
+ if (value <= thresholds[1]) {
1370
+ return 2;
1371
+ }
1372
+ if (value <= thresholds[2]) {
1373
+ return 3;
1374
+ }
1375
+ if (value <= thresholds[3]) {
1376
+ return 4;
1377
+ }
1378
+ return 5;
1379
+ }
1380
+
1381
+ function createSegmentedButtons(container, options, selectedKey, onClick) {
1382
+ container.innerHTML = "";
1383
+ for (const option of options) {
1384
+ const button = document.createElement("button");
1385
+ button.type = "button";
1386
+ button.textContent = option.label;
1387
+ if (option.key === selectedKey) {
1388
+ button.classList.add("is-active");
1389
+ }
1390
+ button.addEventListener("click", () => onClick(option.key));
1391
+ container.appendChild(button);
1392
+ }
1393
+ }
1394
+
1395
+ function updateTooltip(content, x, y) {
1396
+ elements.tooltip.innerHTML = content;
1397
+ elements.tooltip.classList.add("is-visible");
1398
+
1399
+ const pad = 18;
1400
+ const width = elements.tooltip.offsetWidth;
1401
+ const height = elements.tooltip.offsetHeight;
1402
+ const nextX = Math.min(window.innerWidth - width - pad, x + 16);
1403
+ const nextY = Math.min(window.innerHeight - height - pad, y + 16);
1404
+ elements.tooltip.style.transform = `translate(${Math.max(pad, nextX)}px, ${Math.max(pad, nextY)}px)`;
1405
+ }
1406
+
1407
+ function hideTooltip() {
1408
+ elements.tooltip.classList.remove("is-visible");
1409
+ }
1410
+
1411
+ function renderWeekdayLabels() {
1412
+ elements.weekdayColumn.innerHTML = "";
1413
+ weekdayNames.forEach((name, index) => {
1414
+ const label = document.createElement("div");
1415
+ label.textContent = index === 0 || index === 6 ? name : "";
1416
+ label.className = label.textContent ? "" : "weekday-spacer";
1417
+ elements.weekdayColumn.appendChild(label);
1418
+ });
1419
+ }
1420
+
1421
+ function renderLegend() {
1422
+ elements.legendScale.innerHTML = "";
1423
+ for (let index = 0; index <= 5; index += 1) {
1424
+ const swatch = document.createElement("div");
1425
+ swatch.className = "legend-cell";
1426
+ swatch.style.background = `var(--cell-${index})`;
1427
+ elements.legendScale.appendChild(swatch);
1428
+ }
1429
+ }
1430
+
1431
+ function renderMonths(visibleCalendar, rangeStart) {
1432
+ elements.monthsRow.innerHTML = "";
1433
+ const rootStyle = getComputedStyle(document.documentElement);
1434
+ const cell = parseFloat(rootStyle.getPropertyValue("--cell-size")) || 14;
1435
+ const gap = parseFloat(rootStyle.getPropertyValue("--cell-gap")) || 4;
1436
+ const stride = cell + gap;
1437
+
1438
+ const monthPositions = [];
1439
+ const used = new Set();
1440
+ const startMonth = rangeStart.slice(0, 7);
1441
+ const startIndex = Math.max(visibleCalendar.dates.indexOf(rangeStart), 0);
1442
+ monthPositions.push({
1443
+ columnIndex: Math.floor(startIndex / 7),
1444
+ date: rangeStart,
1445
+ });
1446
+ used.add(startMonth);
1447
+
1448
+ for (let index = 0; index < visibleCalendar.dates.length; index += 1) {
1449
+ const dateKey = visibleCalendar.dates[index];
1450
+ if (dateKey < rangeStart) {
1451
+ continue;
1452
+ }
1453
+ if (!dateKey.endsWith("-01")) {
1454
+ continue;
1455
+ }
1456
+ const month = dateKey.slice(0, 7);
1457
+ if (used.has(month)) {
1458
+ continue;
1459
+ }
1460
+ used.add(month);
1461
+ monthPositions.push({
1462
+ columnIndex: Math.floor(index / 7),
1463
+ date: dateKey,
1464
+ });
1465
+ }
1466
+
1467
+ for (const entry of monthPositions) {
1468
+ const label = document.createElement("div");
1469
+ label.className = "month-label";
1470
+ label.textContent = monthLabel.format(parseIsoDate(entry.date));
1471
+ label.style.left = `${entry.columnIndex * stride}px`;
1472
+ elements.monthsRow.appendChild(label);
1473
+ }
1474
+ }
1475
+
1476
+ function renderHeatmap() {
1477
+ const activeReport = getActiveReport();
1478
+ const dataByDay = new Map((activeReport.days || []).map((entry) => [entry.date, entry]));
1479
+ const visibleDays = getVisibleDays();
1480
+ const visibleCalendar = getVisibleCalendar();
1481
+ const range = getCurrentRange();
1482
+ const values = visibleDays.map((entry) => getHeatmapMetricValue(entry));
1483
+ const thresholds = buildThresholds(values);
1484
+
1485
+ const visibleSet = new Set(visibleDays.map((entry) => entry.date));
1486
+ const weekColumns = [];
1487
+
1488
+ for (let index = 0; index < visibleCalendar.dates.length; index += 7) {
1489
+ const week = [];
1490
+ for (let row = 0; row < 7; row += 1) {
1491
+ const dateKey = visibleCalendar.dates[index + row];
1492
+ const entry = dataByDay.get(dateKey);
1493
+ const isPadding = !visibleSet.has(dateKey);
1494
+ const value = entry ? getHeatmapMetricValue(entry) : 0;
1495
+ week.push({
1496
+ date: dateKey,
1497
+ entry,
1498
+ isPadding,
1499
+ value,
1500
+ level: levelForValue(value, thresholds),
1501
+ });
1502
+ }
1503
+ weekColumns.push(week);
1504
+ }
1505
+
1506
+ elements.heatmapGrid.innerHTML = "";
1507
+ for (const week of weekColumns) {
1508
+ for (const day of week) {
1509
+ const cell = document.createElement("div");
1510
+ cell.className = "day-cell";
1511
+ if (day.isPadding) {
1512
+ cell.classList.add("is-padding");
1513
+ }
1514
+ cell.dataset.level = String(day.level);
1515
+
1516
+ if (!day.isPadding && day.entry) {
1517
+ const topModels = Object.entries(day.entry.modelTotals || {})
1518
+ .sort((a, b) => b[1] - a[1])
1519
+ .slice(0, 3)
1520
+ .map(([model, value]) => `${model}: ${formatCompactNumber(value)}`)
1521
+ .join("<br />");
1522
+
1523
+ const isActivityOnly = (day.entry.totalTokens || 0) <= 0 && (day.entry.displayValue || 0) > 0;
1524
+ const tooltipHtml = isActivityOnly
1525
+ ? [
1526
+ `<p class="tooltip-title">${formatDate(day.date)}</p>`,
1527
+ `<p class="tooltip-line"><strong>Legacy activity only</strong></p>`,
1528
+ `<p class="tooltip-line">${formatCompactNumber(day.entry.displayValue || 0)} history event(s)</p>`,
1529
+ `<p class="tooltip-line">No token or cost totals are available for this older Claude day.</p>`,
1530
+ ].join("")
1531
+ : [
1532
+ `<p class="tooltip-title">${formatDate(day.date)}</p>`,
1533
+ `<p class="tooltip-line"><strong>${formatCompactNumber(day.entry.totalTokens)}</strong> total tokens</p>`,
1534
+ `<p class="tooltip-line"><strong>${formatCurrency(day.entry.costUSD || 0)}</strong> estimated cost</p>`,
1535
+ `<p class="tooltip-line">${formatCompactNumber(day.entry.inputTokens)} input, ${formatCompactNumber(day.entry.cachedInputTokens)} cached, ${formatCompactNumber(day.entry.outputTokens)} output</p>`,
1536
+ `<p class="tooltip-line">${formatCompactNumber(day.entry.reasoningTokens)} reasoning, ${day.entry.events} token events</p>`,
1537
+ topModels ? `<p class="tooltip-line"><strong>Models</strong><br />${topModels}</p>` : "",
1538
+ ].join("");
1539
+
1540
+ cell.addEventListener("mousemove", (event) => {
1541
+ updateTooltip(tooltipHtml, event.clientX, event.clientY);
1542
+ });
1543
+ cell.addEventListener("mouseleave", hideTooltip);
1544
+ }
1545
+
1546
+ elements.heatmapGrid.appendChild(cell);
1547
+ }
1548
+ }
1549
+
1550
+ renderMonths(visibleCalendar, range.start);
1551
+ const providerLabel = activeReport.providerShortLabel || activeReport.providerLabel || state.provider;
1552
+ elements.rangeNote.textContent = `${providerLabel} · ${range.label} view · ${range.start} to ${range.end} · heatmap metric: ${METRIC_OPTIONS.find((entry) => entry.key === state.metric)?.label ?? state.metric}`;
1553
+ }
1554
+
1555
+ function renderSummary() {
1556
+ const activeReport = getActiveReport();
1557
+ const visibleDays = getVisibleDays();
1558
+ const range = getCurrentRange();
1559
+ const rangeStart = parseIsoDate(range.start);
1560
+ const rangeEnd = parseIsoDate(range.end);
1561
+ const rangeLength = Math.round((rangeEnd - rangeStart) / 86400000) + 1;
1562
+
1563
+ const inputTotal = sumField(visibleDays, "inputTokens");
1564
+ const outputTotal = sumField(visibleDays, "outputTokens");
1565
+ const totalTokens = sumField(visibleDays, "totalTokens");
1566
+ const totalCost = visibleDays.reduce((sum, entry) => sum + (entry.costUSD || 0), 0);
1567
+ const activeDays = visibleDays.filter((entry) => entry.totalTokens > 0).length;
1568
+ const averagePerDay = rangeLength > 0 ? totalTokens / rangeLength : 0;
1569
+ const averageActiveDay = activeDays > 0 ? totalTokens / activeDays : 0;
1570
+ const averageCostPerDay = rangeLength > 0 ? totalCost / rangeLength : 0;
1571
+ const averageCostActiveDay = activeDays > 0 ? totalCost / activeDays : 0;
1572
+
1573
+ elements.inputTotal.textContent = formatCompactNumber(inputTotal);
1574
+ elements.outputTotal.textContent = formatCompactNumber(outputTotal);
1575
+ elements.tokenTotal.textContent = formatCompactNumber(totalTokens);
1576
+ elements.averagePerDay.textContent = formatCompactNumber(averagePerDay);
1577
+ elements.averageDetail.textContent = activeDays
1578
+ ? `${activeDays} active days · ${formatCompactNumber(averageActiveDay)} on active days`
1579
+ : "No activity in the selected range";
1580
+
1581
+ const topModel = getTopModel(visibleDays);
1582
+ elements.topModel.textContent = topModel ? topModel[0] : "-";
1583
+ elements.topModelDetail.textContent = topModel
1584
+ ? `${formatCompactNumber(topModel[1])} tokens in the current view`
1585
+ : "No model data available";
1586
+
1587
+ const recentModel = getTopModel(getLast30Days(range.end, activeReport.days || []));
1588
+ elements.recentModel.textContent = recentModel ? recentModel[0] : "-";
1589
+ elements.recentModelDetail.textContent = recentModel
1590
+ ? `${formatCompactNumber(recentModel[1])} tokens over the last 30 days`
1591
+ : "No activity in the last 30 days";
1592
+
1593
+ const streaks = computeStreaks();
1594
+ elements.longestStreak.textContent = `${streaks.longestCount} days`;
1595
+ elements.longestStreakDetail.textContent = streaks.longestEnd
1596
+ ? `ended ${streaks.longestEnd}`
1597
+ : "No streaks yet";
1598
+ elements.currentStreak.textContent = `${streaks.currentCount} days`;
1599
+ elements.currentStreakDetail.textContent = streaks.currentCount
1600
+ ? `through ${streaks.currentEnd}`
1601
+ : "No activity on the latest day";
1602
+
1603
+ elements.estimatedCost.textContent = formatCurrency(totalCost);
1604
+ elements.estimatedCostDetail.textContent = activeDays
1605
+ ? `${formatCurrency(averageCostPerDay)} / calendar day · ${formatCurrency(averageCostActiveDay)} / active day`
1606
+ : "No priced activity in the selected range";
1607
+
1608
+ renderCostTable(visibleDays, totalCost);
1609
+
1610
+ const activeScan = activeReport.scan || {};
1611
+ const activePricing = activeReport.pricing || {};
1612
+ if (state.provider === "all") {
1613
+ elements.subtitle.textContent = `Combined daily token usage across ${PROVIDER_ORDER.length.toLocaleString("en-US")} providers in ${REPORT.timezone}.`;
1614
+ } else {
1615
+ elements.subtitle.textContent = `Daily token usage extracted from ${Number(activeScan.filesScanned || 0).toLocaleString("en-US")} local usage files for ${activeReport.providerLabel || state.provider} in ${REPORT.timezone}.`;
1616
+ }
1617
+ const scanSummaryParts = [
1618
+ `${Number(activeScan.tokenEventsCounted || 0).toLocaleString("en-US")} token events counted`,
1619
+ `${Number(activeScan.nullInfoEventsSkipped || 0).toLocaleString("en-US")} rate-limit-only events skipped`,
1620
+ `${Number(activeScan.syntheticEventsSkipped || 0).toLocaleString("en-US")} synthetic events skipped`,
1621
+ ];
1622
+ if (Number(activeScan.activityOnlyDays || 0) > 0) {
1623
+ scanSummaryParts.push(
1624
+ `${Number(activeScan.activityOnlyDays || 0).toLocaleString("en-US")} legacy activity-only days shown`
1625
+ );
1626
+ }
1627
+ elements.scanSummary.textContent = scanSummaryParts.join(" · ");
1628
+ const missingPricing = (activePricing.missingModels || []).length;
1629
+ elements.pricingNote.textContent = `Pricing: ${activePricing.sourceLabel || "unknown"}`;
1630
+ elements.costTableNote.textContent = missingPricing
1631
+ ? `${missingPricing} model(s) missing pricing and counted at $0.00`
1632
+ : "Estimated from model token pricing";
1633
+ const missingModels = activePricing.missingModels || [];
1634
+ if (elements.missingPricingDetails && elements.missingPricingWrap && elements.missingPricingTooltip) {
1635
+ if (missingModels.length > 0) {
1636
+ elements.missingPricingWrap.hidden = false;
1637
+ elements.missingPricingTooltip.innerHTML = `
1638
+ <strong>Missing pricing models</strong>
1639
+ <ul>${missingModels.map((model) => `<li>${model}</li>`).join("")}</ul>
1640
+ `;
1641
+ } else {
1642
+ elements.missingPricingWrap.hidden = true;
1643
+ elements.missingPricingTooltip.innerHTML = "";
1644
+ }
1645
+ }
1646
+ elements.generationSummary.textContent = `Generated ${REPORT.generatedAtDisplay} on ${REPORT.platform}`;
1647
+ }
1648
+
1649
+ function renderCostTable(visibleDays, totalCost) {
1650
+ const rows = [...visibleDays]
1651
+ .filter((entry) => (entry.totalTokens || 0) > 0)
1652
+ .sort((a, b) => b.date.localeCompare(a.date));
1653
+
1654
+ elements.costTableBody.innerHTML = "";
1655
+ for (const entry of rows) {
1656
+ const row = document.createElement("tr");
1657
+ const topModel = (entry.modelBreakdown || [])[0];
1658
+ row.innerHTML = `
1659
+ <td>${entry.date}</td>
1660
+ <td>${formatCurrency(entry.costUSD || 0)}</td>
1661
+ <td>${formatCompactNumber(entry.totalTokens || 0)}</td>
1662
+ <td>${formatCompactNumber(entry.inputTokens || 0)}</td>
1663
+ <td>${formatCompactNumber(entry.cachedInputTokens || 0)}</td>
1664
+ <td>${formatCompactNumber(entry.outputTokens || 0)}</td>
1665
+ <td>${topModel ? `${topModel.name} (${formatCurrency(topModel.costUSD || 0)})` : "-"}</td>
1666
+ `;
1667
+ elements.costTableBody.appendChild(row);
1668
+ }
1669
+
1670
+ const modelCount = new Set();
1671
+ for (const entry of visibleDays) {
1672
+ for (const model of Object.keys(entry.modelTotals || {})) {
1673
+ modelCount.add(model);
1674
+ }
1675
+ }
1676
+
1677
+ elements.costTableTotalCost.textContent = formatCurrency(totalCost);
1678
+ elements.costTableTotalTokens.textContent = formatCompactNumber(
1679
+ visibleDays.reduce((sum, entry) => sum + (entry.totalTokens || 0), 0)
1680
+ );
1681
+ elements.costTableTotalInput.textContent = formatCompactNumber(
1682
+ visibleDays.reduce((sum, entry) => sum + (entry.inputTokens || 0), 0)
1683
+ );
1684
+ elements.costTableTotalCached.textContent = formatCompactNumber(
1685
+ visibleDays.reduce((sum, entry) => sum + (entry.cachedInputTokens || 0), 0)
1686
+ );
1687
+ elements.costTableTotalOutput.textContent = formatCompactNumber(
1688
+ visibleDays.reduce((sum, entry) => sum + (entry.outputTokens || 0), 0)
1689
+ );
1690
+ elements.costTableTotalModels.textContent = `${modelCount.size} model(s)`;
1691
+ }
1692
+
1693
+ function renderMonthlyComparison() {
1694
+ if (!elements.monthlyComparisonSection) {
1695
+ return;
1696
+ }
1697
+
1698
+ const activeReport = getActiveReport();
1699
+ const comparisonProviderId =
1700
+ activeReport && (activeReport.providerId === "codex" || activeReport.providerId === "claude")
1701
+ ? activeReport.providerId
1702
+ : null;
1703
+ const comparisonReport = comparisonProviderId ? PROVIDER_REPORTS[comparisonProviderId] : null;
1704
+ const monthlyRows = [...((comparisonReport && comparisonReport.monthly) || [])].sort((a, b) =>
1705
+ b.month.localeCompare(a.month)
1706
+ );
1707
+ const shouldShow =
1708
+ comparisonProviderId &&
1709
+ monthlyRows.length > 0;
1710
+
1711
+ elements.monthlyComparisonSection.hidden = !shouldShow;
1712
+ if (!shouldShow) {
1713
+ return;
1714
+ }
1715
+
1716
+ const providerLabel = comparisonReport.providerLabel || comparisonProviderId;
1717
+ const planCost = Math.max(getProviderPlanCost(comparisonProviderId), 0);
1718
+ if (elements.monthlyComparisonTitle) {
1719
+ elements.monthlyComparisonTitle.textContent = `${providerLabel} Monthly Spend vs Plan`;
1720
+ }
1721
+ if (elements.providerPlanCostInput) {
1722
+ elements.providerPlanCostInput.value = String(Number(planCost.toFixed(2)));
1723
+ }
1724
+
1725
+ const currentMonth = monthlyRows[0];
1726
+ const currentDelta = (currentMonth?.costUSD || 0) - planCost;
1727
+ const monthsOver = monthlyRows.filter((entry) => (entry.costUSD || 0) > planCost).length;
1728
+ const totalSpend = monthlyRows.reduce((sum, entry) => sum + (entry.costUSD || 0), 0);
1729
+ const totalPlan = monthlyRows.length * planCost;
1730
+ const totalDelta = totalSpend - totalPlan;
1731
+ const totalTokens = monthlyRows.reduce((sum, entry) => sum + (entry.totalTokens || 0), 0);
1732
+
1733
+ elements.monthlyComparisonNote.textContent = `${monthlyRows.length} month(s) of ${providerLabel} spend compared against an editable monthly plan cost.`;
1734
+ elements.monthlyCurrentSpend.textContent = formatCurrency(currentMonth?.costUSD || 0);
1735
+ elements.monthlyCurrentSpendDetail.textContent = currentMonth
1736
+ ? `${formatMonth(currentMonth.month)} · ${formatCompactNumber(currentMonth.totalTokens || 0)} tokens`
1737
+ : "No monthly data";
1738
+ elements.monthlyCurrentDelta.textContent = formatCurrency(Math.abs(currentDelta));
1739
+ elements.monthlyCurrentDelta.classList.toggle("delta-over", currentDelta > 0);
1740
+ elements.monthlyCurrentDelta.classList.toggle("delta-under", currentDelta < 0);
1741
+ elements.monthlyCurrentDeltaDetail.textContent = currentMonth
1742
+ ? currentDelta > 0
1743
+ ? `${formatMonth(currentMonth.month)} is over plan by ${formatCurrency(currentDelta)}`
1744
+ : currentDelta < 0
1745
+ ? `${formatMonth(currentMonth.month)} is under plan by ${formatCurrency(Math.abs(currentDelta))}`
1746
+ : `${formatMonth(currentMonth.month)} matches the plan`
1747
+ : "No monthly data";
1748
+ elements.monthlyOverCount.textContent = monthsOver.toLocaleString("en-US");
1749
+ elements.monthlyOverCountDetail.textContent = `${(monthlyRows.length - monthsOver).toLocaleString("en-US")} month(s) at or under plan`;
1750
+
1751
+ elements.monthlyTableBody.innerHTML = "";
1752
+ for (const entry of monthlyRows) {
1753
+ const row = document.createElement("tr");
1754
+ const delta = (entry.costUSD || 0) - planCost;
1755
+ const topModel = (entry.modelBreakdown || [])[0];
1756
+ const deltaClass = delta > 0 ? "delta-over" : delta < 0 ? "delta-under" : "";
1757
+ row.innerHTML = `
1758
+ <td>${formatMonth(entry.month)}</td>
1759
+ <td>${formatCurrency(entry.costUSD || 0)}</td>
1760
+ <td>${formatCurrency(planCost)}</td>
1761
+ <td class="${deltaClass}">${delta === 0 ? formatCurrency(0) : `${delta > 0 ? "+" : "-"}${formatCurrency(Math.abs(delta))}`}</td>
1762
+ <td>${formatCompactNumber(entry.totalTokens || 0)}</td>
1763
+ <td>${topModel ? `${topModel.name} (${formatCurrency(topModel.costUSD || 0)})` : "-"}</td>
1764
+ `;
1765
+ elements.monthlyTableBody.appendChild(row);
1766
+ }
1767
+
1768
+ elements.monthlyTableTotalSpend.textContent = formatCurrency(totalSpend);
1769
+ elements.monthlyTableTotalPlan.textContent = formatCurrency(totalPlan);
1770
+ elements.monthlyTableTotalDelta.textContent = `${totalDelta > 0 ? "+" : totalDelta < 0 ? "-" : ""}${formatCurrency(Math.abs(totalDelta))}`;
1771
+ elements.monthlyTableTotalDelta.classList.toggle("delta-over", totalDelta > 0);
1772
+ elements.monthlyTableTotalDelta.classList.toggle("delta-under", totalDelta < 0);
1773
+ elements.monthlyTableTotalTokens.textContent = formatCompactNumber(totalTokens);
1774
+ elements.monthlyTableTotalMonths.textContent = `${monthlyRows.length} month(s)`;
1775
+ }
1776
+
1777
+ function renderControls() {
1778
+ if (elements.providerControls) {
1779
+ if (PROVIDER_OPTIONS.length > 1) {
1780
+ elements.providerControls.hidden = false;
1781
+ createSegmentedButtons(
1782
+ elements.providerControls,
1783
+ PROVIDER_OPTIONS,
1784
+ state.provider,
1785
+ (key) => {
1786
+ state.provider = key;
1787
+ render();
1788
+ }
1789
+ );
1790
+ } else {
1791
+ elements.providerControls.hidden = true;
1792
+ elements.providerControls.innerHTML = "";
1793
+ }
1794
+ }
1795
+
1796
+ createSegmentedButtons(elements.rangeControls, RANGE_OPTIONS, state.range, (key) => {
1797
+ state.range = key;
1798
+ render();
1799
+ });
1800
+
1801
+ createSegmentedButtons(elements.metricControls, METRIC_OPTIONS, state.metric, (key) => {
1802
+ state.metric = key;
1803
+ render();
1804
+ });
1805
+ }
1806
+
1807
+ function render() {
1808
+ renderControls();
1809
+ renderWeekdayLabels();
1810
+ renderLegend();
1811
+ renderHeatmap();
1812
+ renderSummary();
1813
+ renderMonthlyComparison();
1814
+ }
1815
+
1816
+ if (elements.providerPlanCostInput) {
1817
+ elements.providerPlanCostInput.addEventListener("input", (event) => {
1818
+ const activeReport = getActiveReport();
1819
+ const comparisonProviderId =
1820
+ activeReport && (activeReport.providerId === "codex" || activeReport.providerId === "claude")
1821
+ ? activeReport.providerId
1822
+ : null;
1823
+ if (!comparisonProviderId) {
1824
+ return;
1825
+ }
1826
+ const nextValue = Number.parseFloat(event.target.value);
1827
+ const value = Number.isFinite(nextValue) && nextValue >= 0 ? nextValue : 0;
1828
+ setProviderPlanCost(comparisonProviderId, value);
1829
+ renderMonthlyComparison();
1830
+ });
1831
+ }
1832
+
1833
+ window.addEventListener("resize", render);
1834
+ render();
1835
+ </script>
1836
+ </body>
1837
+ </html>