squeezr-ai 1.46.2 → 1.46.3
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/dist/dashboard.d.ts +3 -1
- package/dist/dashboard.js +1658 -1655
- package/dist/server.js +997 -992
- package/package.json +1 -1
package/dist/dashboard.js
CHANGED
|
@@ -3,1659 +3,1662 @@
|
|
|
3
3
|
* Pages: Overview + Settings only.
|
|
4
4
|
* Dark/light mode, SSE + polling fallback, zero external deps.
|
|
5
5
|
*/
|
|
6
|
-
export const
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
<
|
|
10
|
-
<
|
|
11
|
-
<
|
|
12
|
-
<
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
--
|
|
20
|
-
--
|
|
21
|
-
--
|
|
22
|
-
--
|
|
23
|
-
--
|
|
24
|
-
--
|
|
25
|
-
--
|
|
26
|
-
--
|
|
27
|
-
--
|
|
28
|
-
--brand
|
|
29
|
-
--
|
|
30
|
-
--
|
|
31
|
-
--
|
|
32
|
-
--
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
--
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
--
|
|
39
|
-
--
|
|
40
|
-
--
|
|
41
|
-
--
|
|
42
|
-
--
|
|
43
|
-
--
|
|
44
|
-
--
|
|
45
|
-
--
|
|
46
|
-
--
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
/* ──
|
|
64
|
-
#
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
.nb-
|
|
85
|
-
|
|
86
|
-
/*
|
|
87
|
-
.nb-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
.nb-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
.
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
.hc-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
.
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
.tool-
|
|
176
|
-
.tool-
|
|
177
|
-
.tool-
|
|
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
|
-
.lim-
|
|
205
|
-
.lim-
|
|
206
|
-
.lim-
|
|
207
|
-
.lim-fill.
|
|
208
|
-
.lim-
|
|
209
|
-
.lim-
|
|
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
|
-
|
|
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
|
-
.action-btn
|
|
270
|
-
.action-
|
|
271
|
-
.action-
|
|
272
|
-
.action-
|
|
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
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
<
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
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
|
-
<line x1="
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
<div class="hc-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
<div class="hc-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
<div class="hc-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
<button class="mode-btn" data-mode="
|
|
398
|
-
<
|
|
399
|
-
<button class="
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
</div>
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
<div style="font-size:11px;
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
<div style="font-size:11px;
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
</div>
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
</div>
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
<
|
|
486
|
-
<button class="mode-btn"
|
|
487
|
-
<
|
|
488
|
-
<button class="mode-btn"
|
|
489
|
-
<
|
|
490
|
-
<
|
|
491
|
-
<button class="mode-btn" onclick="navigatePeriod(
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
<div class="hc-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
<div class="hc-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
<div class="hc-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
</div>
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
</div>
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
</div>
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
<span class="s-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
<span class="s-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
<span class="s-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
<span class="s-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
<span class="s-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
<
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
</div>
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
</div>
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
<button class="mode-btn" data-backend="
|
|
615
|
-
<button class="mode-btn" data-backend="
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
</div>
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
</div>
|
|
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
|
-
<div class="chip"><div class="chip-dot"></div>
|
|
655
|
-
<div class="chip"><div class="chip-dot"></div>
|
|
656
|
-
<div class="chip"><div class="chip-dot"></div>
|
|
657
|
-
<div class="chip"><div class="chip-dot"></div>
|
|
658
|
-
<div class="chip"><div class="chip-dot"></div>
|
|
659
|
-
<div class="chip"><div class="chip-dot"></div>
|
|
660
|
-
<div class="chip"><div class="chip-dot"></div>
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
<div
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
<div
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
<div
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
</div>
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
document.
|
|
721
|
-
document.
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
document.
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
return
|
|
774
|
-
}
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
//
|
|
788
|
-
var
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
:
|
|
794
|
-
//
|
|
795
|
-
var
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
var
|
|
799
|
-
|
|
800
|
-
var
|
|
801
|
-
var
|
|
802
|
-
|
|
803
|
-
var
|
|
804
|
-
|
|
805
|
-
var
|
|
806
|
-
var
|
|
807
|
-
//
|
|
808
|
-
var
|
|
809
|
-
var
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
//
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
document.getElementById('h-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
//
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
setTxt(
|
|
862
|
-
setTxt('sp-
|
|
863
|
-
setTxt('sp-
|
|
864
|
-
setTxt('sp-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
//
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
//
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
//
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
setEl(
|
|
882
|
-
setEl('cfg-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
document.getElementById('
|
|
888
|
-
document.getElementById('cfg-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
if (
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
.
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
var
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
var
|
|
950
|
-
var
|
|
951
|
-
var
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
'
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
'
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
'
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
'
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
'claude-opus-4-
|
|
1036
|
-
'claude-opus-4':
|
|
1037
|
-
'claude-opus-
|
|
1038
|
-
'claude-
|
|
1039
|
-
'claude-
|
|
1040
|
-
'claude-
|
|
1041
|
-
'claude-
|
|
1042
|
-
'claude-
|
|
1043
|
-
'claude-
|
|
1044
|
-
'claude-
|
|
1045
|
-
'claude-
|
|
1046
|
-
'claude-3-
|
|
1047
|
-
'claude-
|
|
1048
|
-
|
|
1049
|
-
'
|
|
1050
|
-
'
|
|
1051
|
-
|
|
1052
|
-
'gpt-5-
|
|
1053
|
-
'gpt-5-
|
|
1054
|
-
'gpt-5-4-
|
|
1055
|
-
'gpt-5-
|
|
1056
|
-
'gpt-
|
|
1057
|
-
'gpt-
|
|
1058
|
-
'gpt-
|
|
1059
|
-
'gpt-
|
|
1060
|
-
'gpt-
|
|
1061
|
-
'gpt-4-
|
|
1062
|
-
'gpt-4':
|
|
1063
|
-
'
|
|
1064
|
-
'
|
|
1065
|
-
'
|
|
1066
|
-
'
|
|
1067
|
-
'
|
|
1068
|
-
'
|
|
1069
|
-
'
|
|
1070
|
-
|
|
1071
|
-
'
|
|
1072
|
-
'
|
|
1073
|
-
|
|
1074
|
-
'gemini-2.
|
|
1075
|
-
'gemini-
|
|
1076
|
-
'gemini-
|
|
1077
|
-
}
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
if (
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
var
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
var
|
|
1129
|
-
var
|
|
1130
|
-
var
|
|
1131
|
-
var
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
'
|
|
1137
|
-
|
|
1138
|
-
'<span style="font-size:12px;color:var(--
|
|
1139
|
-
|
|
1140
|
-
'
|
|
1141
|
-
|
|
1142
|
-
'<
|
|
1143
|
-
'</div>' +
|
|
1144
|
-
'<div style="
|
|
1145
|
-
|
|
1146
|
-
'</div>' +
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
.
|
|
1178
|
-
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
function
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
function
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
'
|
|
1238
|
-
|
|
1239
|
-
'<
|
|
1240
|
-
'</div>' +
|
|
1241
|
-
'<div style="
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
document.
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
}
|
|
1289
|
-
|
|
1290
|
-
function setConn(
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
el
|
|
1325
|
-
el
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
var
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
}
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
}
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
var
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
var
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
}).
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
var
|
|
1475
|
-
if (
|
|
1476
|
-
var
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
var
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
var
|
|
1493
|
-
|
|
1494
|
-
if
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
svCost =
|
|
1503
|
-
svCostNote = '
|
|
1504
|
-
}
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
document.getElementById('sv-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
document.getElementById('sv-
|
|
1513
|
-
document.getElementById('sv-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
//
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
var
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
{ start:
|
|
1547
|
-
{ start:
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
var
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
var
|
|
1599
|
-
var
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
//
|
|
1610
|
-
var
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
var
|
|
1625
|
-
var
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
'
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
'
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
'
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
6
|
+
export const LOGO_PATH_D = "M354.982 369.122C349.882 371.592 338.752 371.792 330.442 369.562C314.752 365.342 292.762 350.502 274.462 331.772L269.022 326.202L268.322 308.932C267.942 299.432 267.332 289.862 266.972 287.662C266.612 285.462 265.842 280.742 265.252 277.162C261.922 256.872 253.782 233.162 245.022 218.222C241.322 211.902 240.442 208.162 242.662 208.162C246.992 208.162 272.062 220.332 283.912 228.172C307.882 244.042 340.042 276.312 356.142 300.642C361.992 309.492 368.862 323.942 370.862 331.632C372.842 339.222 372.822 343.952 370.782 350.952C368.862 357.572 361.602 365.922 354.982 369.122ZM218.282 179.832C214.632 182.212 211.352 184.162 210.992 184.162C209.782 184.162 209.192 181.162 209.872 178.472C211.522 171.892 223.622 148.592 229.912 139.892C238.462 128.072 255.812 107.752 262.572 101.652C289.962 76.9417 301.752 68.0317 317.642 60.0417C337.182 50.2217 355.782 51.3217 365.342 62.8817C368.722 66.9617 372.412 77.3217 372.412 82.7217C372.412 92.2417 366.082 109.302 358.222 120.942C352.882 128.862 338.112 146.372 331.782 152.282L327.912 155.902L306.412 157.012C275.532 158.602 257.232 162.282 234.992 171.372C229.442 173.642 221.922 177.442 218.282 179.832ZM192.352 192.912C191.862 194.152 190.962 195.162 190.372 195.162C188.672 195.162 180.862 177.712 177.542 166.472C172.022 147.832 170.142 131.892 170.112 103.662C170.072 54.9617 176.632 25.5317 190.962 10.2117C203.612 -3.30832 223.802 -3.41833 235.432 9.97167C246.502 22.7117 252.932 45.4017 254.122 75.8417L254.712 91.0217L243.802 102.342C216.642 130.552 201.082 156.292 194.952 183.172C194.012 187.292 192.842 191.672 192.352 192.912ZM226.572 421.482C220.292 424.892 210.782 425.902 205.022 423.752C191.282 418.632 180.692 404.292 175.412 383.662C172.812 373.502 170.052 347.602 170.692 339.372L171.192 333.072L181.082 322.872C198.422 304.992 208.702 290.782 219.092 270.362C225.192 258.372 231.412 241.452 231.412 236.842C231.412 233.222 233.922 230.432 236.032 231.722C240.442 234.432 249.472 263.122 253.062 285.842C255.302 300.022 255.592 348.712 253.532 363.662C249.272 394.512 240.142 414.092 226.572 421.482ZM182.052 209.772C186.532 217.502 181.062 217.082 163.912 208.392C143.982 198.302 124.292 183.882 105.542 165.662C69.9124 131.042 51.3724 100.292 53.7824 79.7817C54.5824 72.9217 59.4624 62.9817 63.5724 59.8517C83.1924 44.8917 111.392 54.5117 145.162 87.6917L156.412 98.7517L156.422 107.702C156.442 128.452 159.472 151.202 164.592 169.092C169.372 185.812 172.862 193.952 182.052 209.772ZM96.0524 369.042C86.6124 371.962 77.4224 371.862 70.9124 368.772C60.5924 363.872 53.4124 352.582 53.4124 341.252C53.4124 325.142 66.4924 301.572 87.9324 279.062C93.8424 272.852 96.3824 270.182 99.4924 269.032C101.842 268.152 104.522 268.152 109.242 268.152C131.112 268.132 157.482 264.312 174.912 258.642C183.912 255.722 201.562 247.552 208.502 243.102C211.022 241.482 213.612 240.162 214.252 240.162C216.572 240.162 215.322 246.362 211.052 256.062C201.052 278.772 190.362 293.692 165.912 319.032C140.952 344.912 115.702 362.982 96.0524 369.042ZM368.762 251.622C362.292 252.472 351.732 253.162 345.312 253.162H333.622L328.262 247.552C314.672 233.322 289.892 214.982 271.112 205.242C261.472 200.242 244.592 193.972 237.082 192.602C232.942 191.852 231.502 189.962 233.362 187.722C235.062 185.672 250.402 179.462 259.532 177.132C280.552 171.762 296.062 170.162 326.772 170.172C369.092 170.192 394.642 174.902 410.892 185.692C419.312 191.282 423.272 197.242 425.392 207.512C426.482 212.822 426.382 214.032 424.312 220.512C422.492 226.212 420.942 228.802 416.682 233.292C413.742 236.392 408.742 240.302 405.572 241.992C398.302 245.872 383.912 249.632 368.762 251.622ZM146.412 251.272C136.432 253.162 130.742 253.482 103.412 253.742C80.9524 253.952 69.0424 253.652 61.9124 252.682C28.4924 248.142 11.0524 240.212 3.39238 226.092C0.252382 220.292 -0.0776191 218.932 0.0123809 212.162C0.142381 202.882 1.92238 198.622 8.60238 191.562C20.8324 178.622 39.5124 173.102 76.4624 171.502L91.0124 170.862L104.492 182.762C125.782 201.552 128.872 203.902 142.912 211.932C160.322 221.882 182.112 231.162 188.092 231.162C189.152 231.162 190.902 231.842 191.972 232.662C193.862 234.132 193.832 234.242 190.912 236.652C184.902 241.622 167.932 247.202 146.412 251.272Z";
|
|
7
|
+
export const LOGO_SVG = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 427 425"><path d="${LOGO_PATH_D}" fill="#e6e6e6"/></svg>`;
|
|
8
|
+
export const DASHBOARD_HTML = /* html */ `<!DOCTYPE html>
|
|
9
|
+
<html lang="en" class="dark">
|
|
10
|
+
<head>
|
|
11
|
+
<meta charset="utf-8">
|
|
12
|
+
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
13
|
+
<link rel="icon" type="image/svg+xml" href="/squeezr/favicon.svg">
|
|
14
|
+
<title>Squeezr</title>
|
|
15
|
+
<style>
|
|
16
|
+
*{box-sizing:border-box;margin:0;padding:0}
|
|
17
|
+
|
|
18
|
+
:root{
|
|
19
|
+
--bg:#0a0a0a;
|
|
20
|
+
--surface:#111111;
|
|
21
|
+
--surface2:#161616;
|
|
22
|
+
--surface3:#1c1c1c;
|
|
23
|
+
--border:#222222;
|
|
24
|
+
--border2:#2e2e2e;
|
|
25
|
+
--text:#f0f0f0;
|
|
26
|
+
--text2:#a0a0a0;
|
|
27
|
+
--text3:#606060;
|
|
28
|
+
--brand:#16a34a;
|
|
29
|
+
--brand2:#4ade80;
|
|
30
|
+
--brand-dim:rgba(22,163,74,.12);
|
|
31
|
+
--brand-dim2:rgba(22,163,74,.06);
|
|
32
|
+
--red:#f87171;
|
|
33
|
+
--yellow:#fbbf24;
|
|
34
|
+
--blue:#60a5fa;
|
|
35
|
+
--shadow:0 1px 3px rgba(0,0,0,.5),0 4px 16px rgba(0,0,0,.3);
|
|
36
|
+
}
|
|
37
|
+
html:not(.dark){
|
|
38
|
+
--bg:#f5f5f5;
|
|
39
|
+
--surface:#ffffff;
|
|
40
|
+
--surface2:#fafafa;
|
|
41
|
+
--surface3:#f0f0f0;
|
|
42
|
+
--border:#e0e0e0;
|
|
43
|
+
--border2:#d0d0d0;
|
|
44
|
+
--text:#111111;
|
|
45
|
+
--text2:#555555;
|
|
46
|
+
--text3:#999999;
|
|
47
|
+
--brand-dim:rgba(22,163,74,.08);
|
|
48
|
+
--brand-dim2:rgba(22,163,74,.04);
|
|
49
|
+
--shadow:0 1px 3px rgba(0,0,0,.08),0 4px 16px rgba(0,0,0,.06);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
html,body{
|
|
53
|
+
height:100%;
|
|
54
|
+
background:var(--bg);
|
|
55
|
+
color:var(--text);
|
|
56
|
+
font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Helvetica Neue',sans-serif;
|
|
57
|
+
font-size:14px;line-height:1.5;
|
|
58
|
+
transition:background .2s,color .2s;
|
|
59
|
+
-webkit-font-smoothing:antialiased;
|
|
60
|
+
}
|
|
61
|
+
code{font-family:'Cascadia Code','SF Mono',Consolas,monospace;font-size:.9em}
|
|
62
|
+
|
|
63
|
+
/* ── Layout ── */
|
|
64
|
+
#app{display:flex;flex-direction:column;height:100vh;overflow:hidden}
|
|
65
|
+
|
|
66
|
+
/* ── Top navbar ── */
|
|
67
|
+
#navbar{
|
|
68
|
+
flex-shrink:0;
|
|
69
|
+
height:52px;
|
|
70
|
+
background:var(--surface);
|
|
71
|
+
border-bottom:1px solid var(--border);
|
|
72
|
+
display:flex;align-items:center;
|
|
73
|
+
padding:0 24px;gap:0;
|
|
74
|
+
transition:background .2s,border-color .2s
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/* Logo */
|
|
78
|
+
.nb-brand{
|
|
79
|
+
display:flex;align-items:center;gap:9px;
|
|
80
|
+
margin-right:24px;flex-shrink:0
|
|
81
|
+
}
|
|
82
|
+
.nb-brand svg{width:24px;height:24px;color:var(--brand)}
|
|
83
|
+
.nb-brand-name{font-size:15px;font-weight:700;letter-spacing:-.3px;color:var(--text)}
|
|
84
|
+
.nb-brand-ver{font-size:11px;color:var(--text3);margin-left:6px;margin-top:1px}
|
|
85
|
+
|
|
86
|
+
/* Divider */
|
|
87
|
+
.nb-sep{width:1px;height:22px;background:var(--border2);margin-right:20px;flex-shrink:0}
|
|
88
|
+
|
|
89
|
+
/* Tabs */
|
|
90
|
+
.nb-tabs{display:flex;align-items:stretch;gap:2px;height:100%}
|
|
91
|
+
.nb-tab{
|
|
92
|
+
display:flex;align-items:center;gap:7px;
|
|
93
|
+
padding:0 16px;
|
|
94
|
+
font-size:13px;font-weight:500;color:var(--text2);
|
|
95
|
+
cursor:pointer;user-select:none;
|
|
96
|
+
border-bottom:2px solid transparent;
|
|
97
|
+
transition:color .12s,border-color .12s;
|
|
98
|
+
white-space:nowrap
|
|
99
|
+
}
|
|
100
|
+
.nb-tab:hover{color:var(--text)}
|
|
101
|
+
.nb-tab.active{color:var(--brand);border-bottom-color:var(--brand)}
|
|
102
|
+
.nb-tab svg{width:14px;height:14px;flex-shrink:0;stroke-width:2}
|
|
103
|
+
|
|
104
|
+
/* Right side */
|
|
105
|
+
.nb-right{
|
|
106
|
+
margin-left:auto;display:flex;align-items:center;gap:10px
|
|
107
|
+
}
|
|
108
|
+
.conn-dot{
|
|
109
|
+
width:7px;height:7px;border-radius:50%;background:var(--text3);flex-shrink:0;
|
|
110
|
+
transition:background .3s
|
|
111
|
+
}
|
|
112
|
+
.conn-dot.online{background:var(--brand);box-shadow:0 0 6px var(--brand)}
|
|
113
|
+
.conn-dot.offline{background:var(--red)}
|
|
114
|
+
.conn-label{font-size:12px;color:var(--text3)}
|
|
115
|
+
|
|
116
|
+
.theme-btn{
|
|
117
|
+
display:flex;align-items:center;justify-content:center;
|
|
118
|
+
width:32px;height:32px;border-radius:8px;
|
|
119
|
+
background:none;border:1px solid var(--border2);cursor:pointer;
|
|
120
|
+
color:var(--text2);transition:background .12s,color .12s
|
|
121
|
+
}
|
|
122
|
+
.theme-btn:hover{background:var(--surface3);color:var(--text)}
|
|
123
|
+
|
|
124
|
+
/* ── Main ── */
|
|
125
|
+
#main{
|
|
126
|
+
flex:1;overflow-y:auto;padding:28px 32px;
|
|
127
|
+
background:var(--bg)
|
|
128
|
+
}
|
|
129
|
+
@media(max-width:600px){
|
|
130
|
+
.nb-brand-ver{display:none}
|
|
131
|
+
#main{padding:16px 14px}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.page-header{margin-bottom:24px}
|
|
135
|
+
.page-title{font-size:22px;font-weight:700;letter-spacing:-.4px;color:var(--text)}
|
|
136
|
+
.page-sub{font-size:13px;color:var(--text3);margin-top:3px}
|
|
137
|
+
|
|
138
|
+
/* ── Hero cards ── */
|
|
139
|
+
.hero-grid{
|
|
140
|
+
display:grid;
|
|
141
|
+
grid-template-columns:repeat(auto-fit,minmax(170px,1fr));
|
|
142
|
+
gap:12px;margin-bottom:20px
|
|
143
|
+
}
|
|
144
|
+
.hero-card{
|
|
145
|
+
background:var(--surface);
|
|
146
|
+
border:1px solid var(--border);
|
|
147
|
+
border-radius:12px;padding:18px 20px;
|
|
148
|
+
box-shadow:var(--shadow);
|
|
149
|
+
transition:border-color .15s
|
|
150
|
+
}
|
|
151
|
+
.hero-card:hover{border-color:var(--border2)}
|
|
152
|
+
.hero-card.accent{
|
|
153
|
+
background:var(--brand-dim);
|
|
154
|
+
border-color:rgba(22,163,74,.25)
|
|
155
|
+
}
|
|
156
|
+
.hc-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--text3);margin-bottom:8px}
|
|
157
|
+
.hc-val{font-size:30px;font-weight:800;letter-spacing:-.5px;color:var(--text);line-height:1}
|
|
158
|
+
.hero-card.accent .hc-val{color:var(--brand2)}
|
|
159
|
+
.hc-sub{font-size:11px;color:var(--text3);margin-top:6px}
|
|
160
|
+
|
|
161
|
+
/* ── Sections ── */
|
|
162
|
+
.section{
|
|
163
|
+
background:var(--surface);border:1px solid var(--border);
|
|
164
|
+
border-radius:12px;margin-bottom:16px;overflow:hidden;
|
|
165
|
+
box-shadow:var(--shadow)
|
|
166
|
+
}
|
|
167
|
+
.section-head{
|
|
168
|
+
padding:14px 20px;border-bottom:1px solid var(--border);
|
|
169
|
+
display:flex;align-items:center;justify-content:space-between
|
|
170
|
+
}
|
|
171
|
+
.section-title{font-size:12px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;color:var(--text3)}
|
|
172
|
+
.section-body{padding:16px 20px}
|
|
173
|
+
|
|
174
|
+
/* ── Tools bars ── */
|
|
175
|
+
.tool-row{display:flex;align-items:center;gap:12px;margin-bottom:10px}
|
|
176
|
+
.tool-row:last-child{margin-bottom:0}
|
|
177
|
+
.tool-name{font-size:13px;color:var(--text2);width:90px;flex-shrink:0;font-weight:500}
|
|
178
|
+
.tool-track{flex:1;height:6px;background:var(--surface3);border-radius:3px;overflow:hidden}
|
|
179
|
+
.tool-fill{height:100%;background:var(--brand);border-radius:3px;transition:width .4s}
|
|
180
|
+
.tool-count{font-size:12px;color:var(--text3);width:50px;text-align:right;font-variant-numeric:tabular-nums}
|
|
181
|
+
|
|
182
|
+
/* ── Latency pills ── */
|
|
183
|
+
.lat-row{display:flex;gap:10px;flex-wrap:wrap}
|
|
184
|
+
.lat-pill{
|
|
185
|
+
flex:1;min-width:80px;
|
|
186
|
+
background:var(--surface2);border:1px solid var(--border2);
|
|
187
|
+
border-radius:10px;padding:12px 16px;text-align:center
|
|
188
|
+
}
|
|
189
|
+
.lat-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);display:block;margin-bottom:4px}
|
|
190
|
+
.lat-val{font-size:22px;font-weight:700;color:var(--text)}
|
|
191
|
+
.lat-unit{font-size:11px;color:var(--text3)}
|
|
192
|
+
|
|
193
|
+
/* ── Cache row ── */
|
|
194
|
+
.cache-row{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
|
|
195
|
+
.cache-card{
|
|
196
|
+
background:var(--surface2);border:1px solid var(--border2);
|
|
197
|
+
border-radius:10px;padding:14px;text-align:center
|
|
198
|
+
}
|
|
199
|
+
.cache-label{font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:6px}
|
|
200
|
+
.cache-val{font-size:22px;font-weight:700;color:var(--text)}
|
|
201
|
+
|
|
202
|
+
/* ── Limits ── */
|
|
203
|
+
.limits-grid{display:flex;flex-direction:column;gap:10px}
|
|
204
|
+
.lim-row{display:flex;align-items:center;gap:14px}
|
|
205
|
+
.lim-name{font-size:12px;color:var(--text2);width:100px;flex-shrink:0;font-weight:500}
|
|
206
|
+
.lim-track{flex:1;height:8px;background:var(--surface3);border-radius:4px;overflow:hidden}
|
|
207
|
+
.lim-fill{height:100%;border-radius:4px;transition:width .5s,background .3s}
|
|
208
|
+
.lim-fill.ok{background:var(--brand)}
|
|
209
|
+
.lim-fill.warn{background:var(--yellow)}
|
|
210
|
+
.lim-fill.crit{background:var(--red)}
|
|
211
|
+
.lim-text{font-size:12px;color:var(--text3);width:90px;text-align:right;font-variant-numeric:tabular-nums}
|
|
212
|
+
.lim-nodata{font-size:13px;color:var(--text3);padding:8px 0}
|
|
213
|
+
|
|
214
|
+
/* ── Mode controls ── */
|
|
215
|
+
.controls-row{display:flex;align-items:center;gap:8px;flex-wrap:wrap}
|
|
216
|
+
.mode-btn{
|
|
217
|
+
padding:7px 16px;border-radius:8px;
|
|
218
|
+
border:1px solid var(--border2);background:var(--surface2);
|
|
219
|
+
color:var(--text2);font-size:12px;font-weight:600;font-family:inherit;
|
|
220
|
+
cursor:pointer;transition:all .12s
|
|
221
|
+
}
|
|
222
|
+
.mode-btn:hover{border-color:var(--brand);color:var(--brand)}
|
|
223
|
+
.mode-btn.active{background:var(--brand-dim);border-color:rgba(22,163,74,.4);color:var(--brand2)}
|
|
224
|
+
.mode-btn.active-off{background:rgba(248,113,113,.1);border-color:rgba(248,113,113,.3);color:var(--red)}
|
|
225
|
+
.divider-v{width:1px;height:24px;background:var(--border2)}
|
|
226
|
+
.bypass-btn{
|
|
227
|
+
padding:7px 16px;border-radius:8px;
|
|
228
|
+
border:1px solid var(--border2);background:var(--surface2);
|
|
229
|
+
color:var(--text2);font-size:12px;font-weight:600;font-family:inherit;
|
|
230
|
+
cursor:pointer;transition:all .12s
|
|
231
|
+
}
|
|
232
|
+
.bypass-btn:hover{border-color:var(--yellow);color:var(--yellow)}
|
|
233
|
+
.bypass-btn.active{background:rgba(251,191,36,.08);border-color:rgba(251,191,36,.3);color:var(--yellow)}
|
|
234
|
+
|
|
235
|
+
/* ── Status badges ── */
|
|
236
|
+
.badge-row{display:flex;gap:8px;margin-bottom:14px}
|
|
237
|
+
.badge{
|
|
238
|
+
font-size:11px;font-weight:600;padding:3px 10px;border-radius:20px;
|
|
239
|
+
border:1px solid var(--border2);color:var(--text3);background:var(--surface2)
|
|
240
|
+
}
|
|
241
|
+
.badge.green{background:var(--brand-dim);border-color:rgba(22,163,74,.3);color:var(--brand2)}
|
|
242
|
+
.badge.yellow{background:rgba(251,191,36,.08);border-color:rgba(251,191,36,.25);color:var(--yellow)}
|
|
243
|
+
.badge.red{background:rgba(248,113,113,.08);border-color:rgba(248,113,113,.25);color:var(--red)}
|
|
244
|
+
|
|
245
|
+
/* ── Settings ── */
|
|
246
|
+
.settings-block{
|
|
247
|
+
background:var(--surface);border:1px solid var(--border);
|
|
248
|
+
border-radius:12px;overflow:hidden;margin-bottom:16px;
|
|
249
|
+
box-shadow:var(--shadow)
|
|
250
|
+
}
|
|
251
|
+
.settings-head{
|
|
252
|
+
padding:12px 20px;border-bottom:1px solid var(--border);
|
|
253
|
+
font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.6px;
|
|
254
|
+
color:var(--text3);background:var(--surface2)
|
|
255
|
+
}
|
|
256
|
+
.settings-row{
|
|
257
|
+
display:flex;align-items:center;justify-content:space-between;
|
|
258
|
+
padding:13px 20px;border-bottom:1px solid var(--border);gap:16px
|
|
259
|
+
}
|
|
260
|
+
.settings-row:last-child{border-bottom:none}
|
|
261
|
+
.s-key{font-size:13px;color:var(--text2);font-weight:500}
|
|
262
|
+
.s-val{font-size:13px;color:var(--text);font-family:'Cascadia Code','SF Mono',Consolas,monospace}
|
|
263
|
+
.s-val code{
|
|
264
|
+
background:var(--surface3);padding:2px 8px;border-radius:5px;
|
|
265
|
+
border:1px solid var(--border2);color:var(--text)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/* ── Action buttons ── */
|
|
269
|
+
.action-btn{padding:7px 18px;border-radius:8px;border:1px solid var(--border2);background:var(--surface2);color:var(--text);font-size:13px;font-family:inherit;cursor:pointer;font-weight:500;transition:all .12s}
|
|
270
|
+
.action-btn:hover{border-color:var(--brand);color:var(--brand)}
|
|
271
|
+
.action-btn.danger{border-color:rgba(248,113,113,.3);color:var(--red)}
|
|
272
|
+
.action-btn.danger:hover{background:rgba(248,113,113,.08)}
|
|
273
|
+
.action-result{margin-top:10px;font-size:12px;padding:8px 12px;border-radius:6px;display:none}
|
|
274
|
+
.action-result.ok{background:rgba(22,163,74,.08);color:#4ade80;border:1px solid rgba(22,163,74,.2)}
|
|
275
|
+
.action-result.err{background:rgba(248,113,113,.08);color:#f87171;border:1px solid rgba(248,113,113,.2)}
|
|
276
|
+
|
|
277
|
+
/* ── CLI chips ── */
|
|
278
|
+
.chips{display:flex;flex-wrap:wrap;gap:7px;padding:16px 20px}
|
|
279
|
+
.chip{
|
|
280
|
+
display:flex;align-items:center;gap:5px;
|
|
281
|
+
padding:5px 12px;border-radius:20px;
|
|
282
|
+
background:var(--surface2);border:1px solid var(--border2);
|
|
283
|
+
font-size:12px;color:var(--text2);font-weight:500
|
|
284
|
+
}
|
|
285
|
+
.chip-dot{width:5px;height:5px;border-radius:50%;background:var(--brand);flex-shrink:0}
|
|
286
|
+
|
|
287
|
+
/* ── Chart ── */
|
|
288
|
+
.chart-bar rect.bar{transition:opacity .15s,filter .15s}
|
|
289
|
+
.chart-bar:hover rect.bar{opacity:.85;filter:brightness(1.15)}
|
|
290
|
+
.chart-wrap{background:var(--surface2);border:1px solid var(--border);border-radius:8px;padding:16px 12px 8px}
|
|
291
|
+
|
|
292
|
+
/* ── Skeleton ── */
|
|
293
|
+
.sk{
|
|
294
|
+
background:linear-gradient(90deg,var(--surface2) 25%,var(--surface3) 50%,var(--surface2) 75%);
|
|
295
|
+
background-size:200% 100%;animation:sk 1.4s infinite;border-radius:6px
|
|
296
|
+
}
|
|
297
|
+
@keyframes sk{0%{background-position:200% 0}100%{background-position:-200% 0}}
|
|
298
|
+
|
|
299
|
+
::-webkit-scrollbar{width:5px;height:5px}
|
|
300
|
+
::-webkit-scrollbar-track{background:transparent}
|
|
301
|
+
::-webkit-scrollbar-thumb{background:var(--border2);border-radius:3px}
|
|
302
|
+
</style>
|
|
303
|
+
</head>
|
|
304
|
+
<body>
|
|
305
|
+
<div id="app">
|
|
306
|
+
|
|
307
|
+
<!-- Top navbar -->
|
|
308
|
+
<header id="navbar">
|
|
309
|
+
<div class="nb-brand">
|
|
310
|
+
<svg viewBox="0 0 427 425" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true">
|
|
311
|
+
<path d="${LOGO_PATH_D}" fill="currentColor"/>
|
|
312
|
+
</svg>
|
|
313
|
+
<span class="nb-brand-name">Squeezr</span>
|
|
314
|
+
<span class="nb-brand-ver" id="sb-ver">—</span>
|
|
315
|
+
</div>
|
|
316
|
+
|
|
317
|
+
<div class="nb-sep"></div>
|
|
318
|
+
|
|
319
|
+
<div class="nb-tabs">
|
|
320
|
+
<div class="nb-tab active" data-page="overview" onclick="go('overview')">
|
|
321
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
|
322
|
+
<rect x="3" y="3" width="7" height="7" rx="1.5"/><rect x="14" y="3" width="7" height="7" rx="1.5"/>
|
|
323
|
+
<rect x="3" y="14" width="7" height="7" rx="1.5"/><rect x="14" y="14" width="7" height="7" rx="1.5"/>
|
|
324
|
+
</svg>
|
|
325
|
+
Overview
|
|
326
|
+
</div>
|
|
327
|
+
<div class="nb-tab" data-page="savings" onclick="go('savings')">
|
|
328
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
|
329
|
+
<line x1="12" y1="1" x2="12" y2="23"/><path d="M17 5H9.5a3.5 3.5 0 0 0 0 7h5a3.5 3.5 0 0 1 0 7H6"/>
|
|
330
|
+
</svg>
|
|
331
|
+
Savings
|
|
332
|
+
</div>
|
|
333
|
+
<div class="nb-tab" data-page="settings" onclick="go('settings')">
|
|
334
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round">
|
|
335
|
+
<circle cx="12" cy="12" r="3"/>
|
|
336
|
+
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/>
|
|
337
|
+
</svg>
|
|
338
|
+
Settings
|
|
339
|
+
</div>
|
|
340
|
+
</div>
|
|
341
|
+
|
|
342
|
+
<div class="nb-right">
|
|
343
|
+
<div class="conn-dot" id="conn-dot"></div>
|
|
344
|
+
<span class="conn-label" id="conn-label">Connecting…</span>
|
|
345
|
+
<button class="theme-btn" onclick="toggleTheme()" title="Toggle theme">
|
|
346
|
+
<svg id="theme-icon" width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
|
|
347
|
+
<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/>
|
|
348
|
+
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/>
|
|
349
|
+
<line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/>
|
|
350
|
+
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>
|
|
351
|
+
</svg>
|
|
352
|
+
</button>
|
|
353
|
+
</div>
|
|
354
|
+
</header>
|
|
355
|
+
|
|
356
|
+
<!-- Main -->
|
|
357
|
+
<main id="main">
|
|
358
|
+
|
|
359
|
+
<!-- ── Overview page ── -->
|
|
360
|
+
<div id="page-overview">
|
|
361
|
+
|
|
362
|
+
<!-- Hero stats -->
|
|
363
|
+
<div class="hero-grid">
|
|
364
|
+
<div class="hero-card accent">
|
|
365
|
+
<div class="hc-label">Tokens Saved</div>
|
|
366
|
+
<div class="hc-val" id="h-saved">—</div>
|
|
367
|
+
<div class="hc-sub">of <span id="h-in">—</span> processed</div>
|
|
368
|
+
</div>
|
|
369
|
+
<div class="hero-card">
|
|
370
|
+
<div class="hc-label">Ratio</div>
|
|
371
|
+
<div class="hc-val" id="h-ratio">—</div>
|
|
372
|
+
<div class="hc-sub">compression avg</div>
|
|
373
|
+
</div>
|
|
374
|
+
<div class="hero-card">
|
|
375
|
+
<div class="hc-label">Cost Saved</div>
|
|
376
|
+
<div class="hc-val" id="h-cost">—</div>
|
|
377
|
+
<div class="hc-sub">estimated USD</div>
|
|
378
|
+
</div>
|
|
379
|
+
<div class="hero-card">
|
|
380
|
+
<div class="hc-label">Requests</div>
|
|
381
|
+
<div class="hc-val" id="h-reqs">—</div>
|
|
382
|
+
<div class="hc-sub"><span id="h-comp">—</span> compressed</div>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<!-- Controls -->
|
|
387
|
+
<div class="section">
|
|
388
|
+
<div class="section-head">
|
|
389
|
+
<span class="section-title">Compression Mode</span>
|
|
390
|
+
<div class="badge-row" style="margin:0">
|
|
391
|
+
<span class="badge" id="mode-badge">—</span>
|
|
392
|
+
<span class="badge" id="bypass-badge">—</span>
|
|
393
|
+
</div>
|
|
394
|
+
</div>
|
|
395
|
+
<div class="section-body">
|
|
396
|
+
<div class="controls-row">
|
|
397
|
+
<button class="mode-btn" data-mode="off" onclick="setMode('off')">Off</button>
|
|
398
|
+
<button class="mode-btn" data-mode="low" onclick="setMode('low')">Low</button>
|
|
399
|
+
<button class="mode-btn active" data-mode="normal" onclick="setMode('normal')">Normal</button>
|
|
400
|
+
<button class="mode-btn" data-mode="aggressive" onclick="setMode('aggressive')">Aggressive</button>
|
|
401
|
+
<div class="divider-v"></div>
|
|
402
|
+
<button class="bypass-btn" id="bypass-btn" onclick="toggleBypass()">Toggle Bypass</button>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
</div>
|
|
406
|
+
|
|
407
|
+
<!-- Rate Limits — moved here, right after controls -->
|
|
408
|
+
<div class="section">
|
|
409
|
+
<div class="section-head"><span class="section-title">Rate Limits</span></div>
|
|
410
|
+
<div class="section-body" id="limits-body">
|
|
411
|
+
<div class="lim-nodata">Loading…</div>
|
|
412
|
+
</div>
|
|
413
|
+
</div>
|
|
414
|
+
|
|
415
|
+
<!-- Two-col grid -->
|
|
416
|
+
<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;margin-bottom:16px">
|
|
417
|
+
<!-- Tools -->
|
|
418
|
+
<div class="section" style="margin:0">
|
|
419
|
+
<div class="section-head"><span class="section-title">Top Tools</span></div>
|
|
420
|
+
<div class="section-body" id="tools-body">
|
|
421
|
+
<div class="sk" style="height:14px;margin-bottom:8px"></div>
|
|
422
|
+
<div class="sk" style="height:14px;margin-bottom:8px;width:80%"></div>
|
|
423
|
+
<div class="sk" style="height:14px;width:65%"></div>
|
|
424
|
+
</div>
|
|
425
|
+
</div>
|
|
426
|
+
<!-- Cache -->
|
|
427
|
+
<div class="section" style="margin:0">
|
|
428
|
+
<div class="section-head"><span class="section-title">Cache</span></div>
|
|
429
|
+
<div class="section-body">
|
|
430
|
+
<div class="cache-row">
|
|
431
|
+
<div class="cache-card"><div class="cache-label">Hits</div><div class="cache-val" id="c-hits">—</div></div>
|
|
432
|
+
<div class="cache-card"><div class="cache-label">Misses</div><div class="cache-val" id="c-miss">—</div></div>
|
|
433
|
+
<div class="cache-card"><div class="cache-label">Rate</div><div class="cache-val" id="c-rate">—</div></div>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
</div>
|
|
437
|
+
</div>
|
|
438
|
+
|
|
439
|
+
<!-- Spend: theoretical vs real -->
|
|
440
|
+
<div class="section">
|
|
441
|
+
<div class="section-head"><span class="section-title">Cost Comparison</span><span style="font-size:11px;color:var(--text3)" id="cost-note">per-model pricing</span></div>
|
|
442
|
+
<div class="section-body">
|
|
443
|
+
<div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px">
|
|
444
|
+
<div style="text-align:center">
|
|
445
|
+
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:6px">Without Squeezr</div>
|
|
446
|
+
<div style="font-size:24px;font-weight:700;color:var(--text)" id="sp-without">—</div>
|
|
447
|
+
<div style="font-size:11px;color:var(--text3);margin-top:4px" id="sp-without-tok">—</div>
|
|
448
|
+
</div>
|
|
449
|
+
<div style="text-align:center">
|
|
450
|
+
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:6px">With Squeezr</div>
|
|
451
|
+
<div style="font-size:24px;font-weight:700;color:var(--brand2)" id="sp-with">—</div>
|
|
452
|
+
<div style="font-size:11px;color:var(--text3);margin-top:4px" id="sp-with-tok">—</div>
|
|
453
|
+
</div>
|
|
454
|
+
<div style="text-align:center">
|
|
455
|
+
<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:6px">Saved</div>
|
|
456
|
+
<div style="font-size:24px;font-weight:700;color:var(--brand2)" id="sp-saved">—</div>
|
|
457
|
+
<div style="font-size:11px;color:var(--text3);margin-top:4px" id="sp-saved-pct">—</div>
|
|
458
|
+
</div>
|
|
459
|
+
</div>
|
|
460
|
+
</div>
|
|
461
|
+
</div>
|
|
462
|
+
|
|
463
|
+
<!-- Model breakdown -->
|
|
464
|
+
<div class="section">
|
|
465
|
+
<div class="section-head"><span class="section-title">By model</span><span style="font-size:11px;color:var(--text3)">real pricing per model</span></div>
|
|
466
|
+
<div class="section-body" id="model-body">
|
|
467
|
+
<div style="font-size:13px;color:var(--text3)">No model data yet.</div>
|
|
468
|
+
</div>
|
|
469
|
+
</div>
|
|
470
|
+
|
|
471
|
+
<!-- Savings by client -->
|
|
472
|
+
<div class="section">
|
|
473
|
+
<div class="section-head"><span class="section-title">Savings by client</span></div>
|
|
474
|
+
<div class="section-body" id="client-body-overview">
|
|
475
|
+
<div style="font-size:13px;color:var(--text3)">No data yet — starts after first request.</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
|
|
480
|
+
<!-- ── Savings page ── -->
|
|
481
|
+
<div id="page-savings" style="display:none">
|
|
482
|
+
|
|
483
|
+
<!-- Period selector + navigation -->
|
|
484
|
+
<div style="display:flex;gap:8px;margin-bottom:20px;align-items:center;flex-wrap:wrap">
|
|
485
|
+
<span style="font-size:13px;color:var(--text2);font-weight:500">View:</span>
|
|
486
|
+
<button class="mode-btn active" id="period-day" onclick="setSavingsPeriod('day')">Day</button>
|
|
487
|
+
<button class="mode-btn" id="period-week" onclick="setSavingsPeriod('week')">Week</button>
|
|
488
|
+
<button class="mode-btn" id="period-month" onclick="setSavingsPeriod('month')">Month</button>
|
|
489
|
+
<button class="mode-btn" id="period-all" onclick="setSavingsPeriod('all')">All time</button>
|
|
490
|
+
<div style="flex:1"></div>
|
|
491
|
+
<button class="mode-btn" onclick="navigatePeriod(-1)" title="Previous">◀</button>
|
|
492
|
+
<span id="period-label" style="font-size:13px;color:var(--text);font-weight:600;min-width:160px;text-align:center">—</span>
|
|
493
|
+
<button class="mode-btn" onclick="navigatePeriod(1)" title="Next">▶</button>
|
|
494
|
+
<button class="mode-btn" onclick="navigatePeriod(0)" title="Today">Today</button>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<!-- Period hero -->
|
|
498
|
+
<div class="hero-grid" id="savings-hero">
|
|
499
|
+
<div class="hero-card accent">
|
|
500
|
+
<div class="hc-label">Tokens Saved</div>
|
|
501
|
+
<div class="hc-val" id="sv-tokens">—</div>
|
|
502
|
+
<div class="hc-sub" id="sv-tokens-sub">—</div>
|
|
503
|
+
</div>
|
|
504
|
+
<div class="hero-card">
|
|
505
|
+
<div class="hc-label">Est. Cost Saved</div>
|
|
506
|
+
<div class="hc-val" id="sv-cost">—</div>
|
|
507
|
+
<div class="hc-sub" id="sv-cost-note">per-model pricing</div>
|
|
508
|
+
</div>
|
|
509
|
+
<div class="hero-card">
|
|
510
|
+
<div class="hc-label">Sessions</div>
|
|
511
|
+
<div class="hc-val" id="sv-sessions">—</div>
|
|
512
|
+
<div class="hc-sub" id="sv-requests">—</div>
|
|
513
|
+
</div>
|
|
514
|
+
<div class="hero-card">
|
|
515
|
+
<div class="hc-label">Avg Saving</div>
|
|
516
|
+
<div class="hc-val" id="sv-pct">—</div>
|
|
517
|
+
<div class="hc-sub">per session</div>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<!-- Daily breakdown chart (bar chart) -->
|
|
522
|
+
<div class="section">
|
|
523
|
+
<div class="section-head"><span class="section-title" id="savings-chart-title">Daily breakdown</span></div>
|
|
524
|
+
<div class="section-body" id="savings-chart">
|
|
525
|
+
<div class="sk" style="height:80px"></div>
|
|
526
|
+
</div>
|
|
527
|
+
</div>
|
|
528
|
+
|
|
529
|
+
<!-- By model -->
|
|
530
|
+
<div class="section">
|
|
531
|
+
<div class="section-head"><span class="section-title">By model</span><span style="font-size:11px;color:var(--text3)">real pricing per model</span></div>
|
|
532
|
+
<div class="section-body" id="model-body-savings">
|
|
533
|
+
<div style="font-size:13px;color:var(--text3)">No model data yet.</div>
|
|
534
|
+
</div>
|
|
535
|
+
</div>
|
|
536
|
+
|
|
537
|
+
<!-- By client -->
|
|
538
|
+
<div class="section">
|
|
539
|
+
<div class="section-head"><span class="section-title">By client (current session)</span></div>
|
|
540
|
+
<div class="section-body" id="client-body-savings">
|
|
541
|
+
<div style="font-size:13px;color:var(--text3)">No client data yet.</div>
|
|
542
|
+
</div>
|
|
543
|
+
</div>
|
|
544
|
+
</div>
|
|
545
|
+
|
|
546
|
+
<!-- ── Settings page ── -->
|
|
547
|
+
<div id="page-settings" style="display:none">
|
|
548
|
+
|
|
549
|
+
<div id="update-banner" style="display:none;background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);border-radius:12px;padding:14px 20px;margin-bottom:20px;align-items:center;justify-content:space-between">
|
|
550
|
+
<div>
|
|
551
|
+
<span style="color:#fbbf24;font-weight:600;font-size:13px">Update available</span>
|
|
552
|
+
<span id="update-text" style="color:var(--text2);font-size:13px;margin-left:8px"></span>
|
|
553
|
+
</div>
|
|
554
|
+
<button class="action-btn" onclick="runAction('update')" style="border-color:rgba(251,191,36,.4);color:#fbbf24">Update now</button>
|
|
555
|
+
</div>
|
|
556
|
+
|
|
557
|
+
|
|
558
|
+
<div class="settings-block">
|
|
559
|
+
<div class="settings-head">Proxy endpoints</div>
|
|
560
|
+
<div class="settings-row">
|
|
561
|
+
<span class="s-key" title="Claude Code, Claude Desktop, Aider, OpenCode">Anthropic <span style="font-size:11px;color:var(--text3)">(Claude)</span></span>
|
|
562
|
+
<span class="s-val"><code id="cfg-url-val">—</code></span>
|
|
563
|
+
</div>
|
|
564
|
+
<div class="settings-row">
|
|
565
|
+
<span class="s-key" title="Codex Desktop app, Continue, Cline, Cursor — use openai_base_url">Codex Desktop <span style="font-size:11px;color:var(--text3)">/ OpenAI apps</span></span>
|
|
566
|
+
<span class="s-val"><code id="cfg-oai-val">—</code><span style="font-size:11px;color:var(--text3);margin-left:6px">/v1 · openai_base_url</span></span>
|
|
567
|
+
</div>
|
|
568
|
+
<div class="settings-row">
|
|
569
|
+
<span class="s-key" title="Gemini CLI — GEMINI_API_BASE_URL">Gemini CLI</span>
|
|
570
|
+
<span class="s-val"><code id="cfg-gem-val">—</code><span style="font-size:11px;color:var(--text3);margin-left:6px">GEMINI_API_BASE_URL</span></span>
|
|
571
|
+
</div>
|
|
572
|
+
<div class="settings-row">
|
|
573
|
+
<span class="s-key" title="Codex CLI (terminal) — WebSocket TLS intercept, set HTTPS_PROXY per session">Codex CLI <span style="font-size:11px;color:var(--text3)">(terminal)</span></span>
|
|
574
|
+
<span class="s-val"><code id="cfg-mitm-val">—</code><span style="font-size:11px;color:var(--text3);margin-left:6px">HTTPS_PROXY · TLS intercept</span></span>
|
|
575
|
+
</div>
|
|
576
|
+
<div class="settings-row">
|
|
577
|
+
<span class="s-key">Version</span>
|
|
578
|
+
<span class="s-val" id="cfg-ver">—</span>
|
|
579
|
+
</div>
|
|
580
|
+
<div class="settings-row">
|
|
581
|
+
<span class="s-key">Uptime</span>
|
|
582
|
+
<span class="s-val" id="cfg-uptime" style="color:var(--brand2)">—</span>
|
|
583
|
+
</div>
|
|
584
|
+
</div>
|
|
585
|
+
|
|
586
|
+
<div class="settings-block">
|
|
587
|
+
<div class="settings-head">Compression</div>
|
|
588
|
+
<div class="settings-row">
|
|
589
|
+
<span class="s-key">Mode</span>
|
|
590
|
+
<span class="s-val"><code id="cfg-mode">—</code></span>
|
|
591
|
+
</div>
|
|
592
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:4px">
|
|
593
|
+
<div style="display:flex;justify-content:space-between;width:100%;align-items:center">
|
|
594
|
+
<span class="s-key">Bypass</span>
|
|
595
|
+
<span class="s-val"><code id="cfg-bypass">—</code></span>
|
|
596
|
+
</div>
|
|
597
|
+
<div style="font-size:12px;color:var(--text3);line-height:1.4">
|
|
598
|
+
When <strong style="color:var(--text2)">enabled</strong>, all requests pass through to the API <em>without compression</em> — useful to check if Squeezr is causing any issue. Stats are still logged. Resets automatically when the proxy restarts.
|
|
599
|
+
</div>
|
|
600
|
+
</div>
|
|
601
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:4px">
|
|
602
|
+
<div style="display:flex;justify-content:space-between;width:100%;align-items:center">
|
|
603
|
+
<span class="s-key">Circuit Breaker</span>
|
|
604
|
+
<span class="s-val"><code id="cfg-cb">—</code></span>
|
|
605
|
+
</div>
|
|
606
|
+
<div style="font-size:12px;color:var(--text3);line-height:1.4">
|
|
607
|
+
Protects against latency spikes. If the local AI compression model (Ollama) fails <strong style="color:var(--text2)">3 times in a row</strong>, it auto-disables AI compression and falls back to deterministic rules only. Returns to normal after 60s without errors. Deterministic compression always stays active.
|
|
608
|
+
</div>
|
|
609
|
+
</div>
|
|
610
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:8px">
|
|
611
|
+
<div style="display:flex;justify-content:space-between;width:100%;align-items:center;flex-wrap:wrap;gap:6px">
|
|
612
|
+
<span class="s-key">Compression backend</span>
|
|
613
|
+
<div style="display:flex;gap:4px;flex-wrap:wrap">
|
|
614
|
+
<button class="mode-btn" data-backend="auto" onclick="setBackend('auto')">Auto</button>
|
|
615
|
+
<button class="mode-btn" data-backend="local" onclick="setBackend('local')">squeezr-1B</button>
|
|
616
|
+
<button class="mode-btn" data-backend="haiku" onclick="setBackend('haiku')">Haiku</button>
|
|
617
|
+
<button class="mode-btn" data-backend="gpt-mini" onclick="setBackend('gpt-mini')">GPT-4o-mini</button>
|
|
618
|
+
<button class="mode-btn" data-backend="gemini-flash" onclick="setBackend('gemini-flash')">Gemini Flash</button>
|
|
619
|
+
</div>
|
|
620
|
+
</div>
|
|
621
|
+
<div style="font-size:12px;color:var(--text3);line-height:1.4">
|
|
622
|
+
Modelo que comprime los tool results y mensajes históricos. <strong style="color:var(--text2)">Auto</strong> usa el modelo de la API que recibe la request (Haiku para Claude, GPT-mini para OpenAI, Flash para Gemini). <strong style="color:var(--text2)">squeezr-1B</strong> es el modelo local (gratis, sin red, requiere Ollama). El resto fuerza ese modelo para todas las requests.
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start;gap:8px">
|
|
626
|
+
<div style="display:flex;justify-content:space-between;width:100%;align-items:center">
|
|
627
|
+
<span class="s-key">Anthropic Native Compact <span style="font-size:10px;background:var(--brand-dim);color:var(--brand2);padding:1px 6px;border-radius:3px;margin-left:4px">beta</span></span>
|
|
628
|
+
<button class="mode-btn" id="native-compact-btn" onclick="toggleNativeCompact()" style="min-width:80px">—</button>
|
|
629
|
+
</div>
|
|
630
|
+
<div style="font-size:12px;color:var(--text3);line-height:1.4">
|
|
631
|
+
Activa el header <code style="font-size:11px">anthropic-beta: compact-2026-01-12</code>. Cuando el contexto excede el threshold, Anthropic <strong style="color:var(--text2)">resume tu conversación automáticamente en sus servidores</strong>. Stacks con la compresión de Squeezr — comprimes primero, ellos resumen lo que queda. <strong>Solo Claude</strong> (no afecta OpenAI/Gemini). Reseteable.
|
|
632
|
+
</div>
|
|
633
|
+
</div>
|
|
634
|
+
</div>
|
|
635
|
+
|
|
636
|
+
<!-- Token savings by client — toggle -->
|
|
637
|
+
<div class="settings-block">
|
|
638
|
+
<div class="settings-head" style="cursor:pointer;display:flex;align-items:center;justify-content:space-between" onclick="toggleClientBreakdown()">
|
|
639
|
+
<span>Token savings by client</span>
|
|
640
|
+
<svg id="cli-chevron" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" style="transition:transform .2s;color:var(--text3)">
|
|
641
|
+
<polyline points="6 9 12 15 18 9"/>
|
|
642
|
+
</svg>
|
|
643
|
+
</div>
|
|
644
|
+
<div id="cli-breakdown" style="display:none">
|
|
645
|
+
<div id="cli-breakdown-body" style="padding:14px 20px">
|
|
646
|
+
<span style="font-size:13px;color:var(--text3)">No client data yet — starts tracking after first request.</span>
|
|
647
|
+
</div>
|
|
648
|
+
</div>
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
<div class="settings-block">
|
|
652
|
+
<div class="settings-head">Connected CLIs & Apps</div>
|
|
653
|
+
<div class="chips">
|
|
654
|
+
<div class="chip"><div class="chip-dot"></div>Claude Code</div>
|
|
655
|
+
<div class="chip"><div class="chip-dot"></div>Claude Desktop</div>
|
|
656
|
+
<div class="chip"><div class="chip-dot"></div>Codex Desktop</div>
|
|
657
|
+
<div class="chip"><div class="chip-dot"></div>Codex CLI</div>
|
|
658
|
+
<div class="chip"><div class="chip-dot"></div>Aider</div>
|
|
659
|
+
<div class="chip"><div class="chip-dot"></div>Gemini CLI</div>
|
|
660
|
+
<div class="chip"><div class="chip-dot"></div>Cursor</div>
|
|
661
|
+
<div class="chip"><div class="chip-dot"></div>Continue.dev</div>
|
|
662
|
+
<div class="chip"><div class="chip-dot"></div>Windsurf</div>
|
|
663
|
+
<div class="chip"><div class="chip-dot"></div>Cline</div>
|
|
664
|
+
</div>
|
|
665
|
+
</div>
|
|
666
|
+
|
|
667
|
+
<div class="settings-block">
|
|
668
|
+
<div class="settings-head">Actions</div>
|
|
669
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start">
|
|
670
|
+
<div style="display:flex;align-items:center;justify-content:space-between;width:100%">
|
|
671
|
+
<span class="s-key">Status</span>
|
|
672
|
+
<button class="action-btn" onclick="runAction('status')">Check Status</button>
|
|
673
|
+
</div>
|
|
674
|
+
<div class="action-result" id="action-result-status"></div>
|
|
675
|
+
</div>
|
|
676
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start">
|
|
677
|
+
<div style="display:flex;align-items:center;justify-content:space-between;width:100%">
|
|
678
|
+
<span class="s-key">Stop Proxy</span>
|
|
679
|
+
<button class="action-btn danger" onclick="runAction('stop')">Stop Proxy</button>
|
|
680
|
+
</div>
|
|
681
|
+
<div class="action-result" id="action-result-stop"></div>
|
|
682
|
+
</div>
|
|
683
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start">
|
|
684
|
+
<div style="display:flex;align-items:center;justify-content:space-between;width:100%">
|
|
685
|
+
<span class="s-key">Update Squeezr</span>
|
|
686
|
+
<button class="action-btn" onclick="runAction('update')">Update to latest</button>
|
|
687
|
+
</div>
|
|
688
|
+
<div class="action-result" id="action-result-update"></div>
|
|
689
|
+
</div>
|
|
690
|
+
<div class="settings-row" style="flex-direction:column;align-items:flex-start">
|
|
691
|
+
<div style="display:flex;align-items:center;justify-content:space-between;width:100%;gap:12px">
|
|
692
|
+
<span class="s-key" style="flex-shrink:0">Ports</span>
|
|
693
|
+
<div style="display:flex;align-items:center;gap:8px;flex:1;justify-content:flex-end">
|
|
694
|
+
<input id="inp-http-port" type="number" placeholder="HTTP" style="width:80px;padding:5px 10px;border-radius:7px;border:1px solid var(--border2);background:var(--surface2);color:var(--text);font-size:12px;font-family:inherit">
|
|
695
|
+
<input id="inp-mitm-port" type="number" placeholder="MITM" style="width:80px;padding:5px 10px;border-radius:7px;border:1px solid var(--border2);background:var(--surface2);color:var(--text);font-size:12px;font-family:inherit">
|
|
696
|
+
<button class="action-btn" onclick="runAction('ports')">Apply</button>
|
|
697
|
+
</div>
|
|
698
|
+
</div>
|
|
699
|
+
<div class="action-result" id="action-result-ports"></div>
|
|
700
|
+
</div>
|
|
701
|
+
</div>
|
|
702
|
+
</div>
|
|
703
|
+
|
|
704
|
+
</main>
|
|
705
|
+
</div>
|
|
706
|
+
|
|
707
|
+
<script>
|
|
708
|
+
// ── Theme ──────────────────────────────────────────────────────────────────
|
|
709
|
+
var MOON = '<circle cx="12" cy="12" r="5"/><line x1="12" y1="1" x2="12" y2="3"/><line x1="12" y1="21" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="5.64" y2="5.64"/><line x1="18.36" y1="18.36" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="3" y2="12"/><line x1="21" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="5.64" y2="18.36"/><line x1="18.36" y1="5.64" x2="19.78" y2="4.22"/>';
|
|
710
|
+
var SUN = '<path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/>';
|
|
711
|
+
|
|
712
|
+
(function(){
|
|
713
|
+
var t = localStorage.getItem('sq-theme') || 'dark';
|
|
714
|
+
setTheme(t, false);
|
|
715
|
+
})();
|
|
716
|
+
|
|
717
|
+
function setTheme(t, save) {
|
|
718
|
+
if (t === 'dark') {
|
|
719
|
+
document.documentElement.classList.add('dark');
|
|
720
|
+
document.getElementById('theme-icon').innerHTML = MOON;
|
|
721
|
+
document.querySelector('.theme-label') && (document.querySelector('.theme-label').textContent = 'Light mode');
|
|
722
|
+
} else {
|
|
723
|
+
document.documentElement.classList.remove('dark');
|
|
724
|
+
document.getElementById('theme-icon').innerHTML = SUN;
|
|
725
|
+
document.querySelector('.theme-label') && (document.querySelector('.theme-label').textContent = 'Dark mode');
|
|
726
|
+
}
|
|
727
|
+
if (save !== false) localStorage.setItem('sq-theme', t);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function toggleTheme() {
|
|
731
|
+
var isDark = document.documentElement.classList.contains('dark');
|
|
732
|
+
setTheme(isDark ? 'light' : 'dark');
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
// ── Navigation ─────────────────────────────────────────────────────────────
|
|
736
|
+
function go(page) {
|
|
737
|
+
document.querySelectorAll('.nb-tab').forEach(function(el) {
|
|
738
|
+
el.classList.toggle('active', el.dataset.page === page);
|
|
739
|
+
});
|
|
740
|
+
document.getElementById('page-overview').style.display = page === 'overview' ? '' : 'none';
|
|
741
|
+
document.getElementById('page-savings').style.display = page === 'savings' ? '' : 'none';
|
|
742
|
+
document.getElementById('page-settings').style.display = page === 'settings' ? '' : 'none';
|
|
743
|
+
if (page === 'savings') loadSavings();
|
|
744
|
+
try { localStorage.setItem('sq-page', page); } catch(e) {}
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Restore last tab on load
|
|
748
|
+
(function(){
|
|
749
|
+
var saved = localStorage.getItem('sq-page');
|
|
750
|
+
if (saved && saved !== 'overview') go(saved);
|
|
751
|
+
})();
|
|
752
|
+
|
|
753
|
+
// ── Helpers ────────────────────────────────────────────────────────────────
|
|
754
|
+
function fmt(n) {
|
|
755
|
+
if (n == null || isNaN(n)) return '—';
|
|
756
|
+
if (n >= 1000000) return (n / 1000000).toFixed(1) + 'M';
|
|
757
|
+
if (n >= 1000) return (n / 1000).toFixed(1) + 'k';
|
|
758
|
+
return String(Math.round(n));
|
|
759
|
+
}
|
|
760
|
+
function fmtUsd(n) {
|
|
761
|
+
if (n == null || isNaN(n) || n === 0) return '—';
|
|
762
|
+
if (n < 0.01) return '<$0.01';
|
|
763
|
+
return '$' + Number(n).toFixed(2);
|
|
764
|
+
}
|
|
765
|
+
function fmtRatio(r) {
|
|
766
|
+
if (r == null) return '—';
|
|
767
|
+
return Math.round((1 - r) * 100) + '%';
|
|
768
|
+
}
|
|
769
|
+
function fmtUptime(s) {
|
|
770
|
+
if (s == null) return '—';
|
|
771
|
+
if (s < 60) return s + 's';
|
|
772
|
+
if (s < 3600) return Math.floor(s/60) + 'm ' + (s%60) + 's';
|
|
773
|
+
return Math.floor(s/3600) + 'h ' + Math.floor((s%3600)/60) + 'm';
|
|
774
|
+
}
|
|
775
|
+
function esc(s) {
|
|
776
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
// ── Render ─────────────────────────────────────────────────────────────────
|
|
780
|
+
var lastStats = null;
|
|
781
|
+
|
|
782
|
+
function render(d) {
|
|
783
|
+
if (!d) return;
|
|
784
|
+
lastStats = d;
|
|
785
|
+
|
|
786
|
+
// ── Normalize field names (API uses snake_case with various naming conventions) ──
|
|
787
|
+
// tokens — server uses CHARS_PER_TOKEN=3.5, match it here for consistency
|
|
788
|
+
var tokensSaved = d.total_saved_tokens || d.tokens_saved || 0;
|
|
789
|
+
var tokensIn = Math.round((d.total_original_chars || 0) / 3.5); // same ratio as stats.ts
|
|
790
|
+
// ratio: API gives savings_pct (0-100), or compression_ratio (0-1)
|
|
791
|
+
var ratioPct = d.savings_pct != null ? d.savings_pct
|
|
792
|
+
: d.compression_ratio != null ? Math.round((1 - d.compression_ratio) * 100)
|
|
793
|
+
: null;
|
|
794
|
+
// cost estimate: if not provided, estimate from saved tokens at ~$3/1M tokens
|
|
795
|
+
var costUsd = d.cost_saved_usd != null ? d.cost_saved_usd
|
|
796
|
+
: tokensSaved > 0 ? tokensSaved * 0.000003 : null;
|
|
797
|
+
// requests
|
|
798
|
+
var reqs = d.requests != null ? d.requests : (d.total_requests || 0);
|
|
799
|
+
var aiComps = d.compressions != null ? d.compressions : (d.compressed || 0);
|
|
800
|
+
var cacheHitsAi = (d.session_cache_hits != null ? d.session_cache_hits : 0);
|
|
801
|
+
var comps = aiComps + cacheHitsAi; // AI calls + session cache reuses
|
|
802
|
+
// latency: nested object { total: { p50, p95, p99 } } or flat
|
|
803
|
+
var lat = (d.latency && d.latency.total) ? d.latency.total : d.latency || {};
|
|
804
|
+
var p50 = lat.p50 != null ? lat.p50 : d.latency_p50;
|
|
805
|
+
var p95 = lat.p95 != null ? lat.p95 : d.latency_p95;
|
|
806
|
+
var p99 = lat.p99 != null ? lat.p99 : d.latency_p99;
|
|
807
|
+
// cache: nested { hits, misses } or flat
|
|
808
|
+
var cacheHits = (d.cache && d.cache.hits != null) ? d.cache.hits : (d.cache_hits || 0);
|
|
809
|
+
var cacheMiss = (d.cache && d.cache.misses != null) ? d.cache.misses : (d.cache_miss || 0);
|
|
810
|
+
// bypass
|
|
811
|
+
var byp = !!(d.bypassed || d.bypass);
|
|
812
|
+
var mode = d.mode || 'normal';
|
|
813
|
+
|
|
814
|
+
// Sidebar version
|
|
815
|
+
if (d.version) document.getElementById('sb-ver').textContent = 'v' + d.version;
|
|
816
|
+
|
|
817
|
+
// Cost comparison (#7) — weighted by actual models used (computed first so hero card can use it)
|
|
818
|
+
var modelCosts = calcCostFromModels(d.by_model, true);
|
|
819
|
+
|
|
820
|
+
// Hero cards
|
|
821
|
+
document.getElementById('h-saved').textContent = fmt(tokensSaved);
|
|
822
|
+
document.getElementById('h-in').textContent = fmt(tokensIn);
|
|
823
|
+
document.getElementById('h-ratio').textContent = ratioPct != null ? Math.round(ratioPct) + '%' : '—';
|
|
824
|
+
// Hero cost: use model-weighted price when available, fall back to flat $3/1M
|
|
825
|
+
var heroCost = (modelCosts && modelCosts.savedCost > 0) ? modelCosts.savedCost : costUsd;
|
|
826
|
+
document.getElementById('h-cost').textContent = fmtUsd(heroCost);
|
|
827
|
+
document.getElementById('h-reqs').textContent = fmt(reqs);
|
|
828
|
+
document.getElementById('h-comp').textContent = fmt(comps);
|
|
829
|
+
|
|
830
|
+
// Latency (elements removed from Overview but kept for potential future use)
|
|
831
|
+
var lp = function(id, v){ var e = document.getElementById(id); if(e) e.textContent = v != null ? v : '—'; };
|
|
832
|
+
lp('l-50', p50); lp('l-95', p95); lp('l-99', p99);
|
|
833
|
+
|
|
834
|
+
// Cache
|
|
835
|
+
var tot = cacheHits + cacheMiss;
|
|
836
|
+
document.getElementById('c-hits').textContent = fmt(cacheHits);
|
|
837
|
+
document.getElementById('c-miss').textContent = fmt(cacheMiss);
|
|
838
|
+
document.getElementById('c-rate').textContent = tot > 0 ? Math.round(cacheHits/tot*100) + '%' : '—';
|
|
839
|
+
|
|
840
|
+
// Tools
|
|
841
|
+
renderTools(d.by_tool || d.tools);
|
|
842
|
+
|
|
843
|
+
// Limits
|
|
844
|
+
renderLimits(d.limits);
|
|
845
|
+
var actualTokens = tokensIn - tokensSaved;
|
|
846
|
+
var costSaved, costWithout, costWith, priceNote;
|
|
847
|
+
if (modelCosts && modelCosts.totalCost > 0) {
|
|
848
|
+
// Precise: model-weighted pricing
|
|
849
|
+
costSaved = modelCosts.savedCost;
|
|
850
|
+
costWithout = modelCosts.totalCost;
|
|
851
|
+
costWith = modelCosts.totalCost - modelCosts.savedCost;
|
|
852
|
+
priceNote = 'model-weighted pricing';
|
|
853
|
+
} else {
|
|
854
|
+
// Fallback: flat $3/1M
|
|
855
|
+
var flat = 0.000003;
|
|
856
|
+
costSaved = tokensSaved * flat;
|
|
857
|
+
costWithout = tokensIn * flat;
|
|
858
|
+
costWith = actualTokens * flat;
|
|
859
|
+
priceNote = 'est. $3/1M (no model data)';
|
|
860
|
+
}
|
|
861
|
+
var setTxt = function(id, v){ var e = document.getElementById(id); if(e) e.textContent = v; };
|
|
862
|
+
setTxt('sp-without', costWithout > 0 ? fmtUsd(costWithout) : '—');
|
|
863
|
+
setTxt('sp-without-tok', tokensIn > 0 ? '~' + fmt(tokensIn) + ' tokens' : '—');
|
|
864
|
+
setTxt('sp-with', costWith > 0 ? fmtUsd(costWith) : '—');
|
|
865
|
+
setTxt('sp-with-tok', actualTokens > 0 ? '~' + fmt(actualTokens) + ' tokens' : '—');
|
|
866
|
+
setTxt('sp-saved', costSaved > 0 ? fmtUsd(costSaved) : '—');
|
|
867
|
+
setTxt('sp-saved-pct', (ratioPct != null ? Math.round(ratioPct) + '% · ' : '') + priceNote);
|
|
868
|
+
var noteEl = document.getElementById('cost-note'); if(noteEl) noteEl.textContent = priceNote;
|
|
869
|
+
// Model breakdown section
|
|
870
|
+
renderModelBreakdown(d.by_model);
|
|
871
|
+
|
|
872
|
+
// CLI breakdown (#8)
|
|
873
|
+
renderClientBreakdown(d.by_client);
|
|
874
|
+
|
|
875
|
+
// Mode & bypass
|
|
876
|
+
updateMode(mode, byp);
|
|
877
|
+
|
|
878
|
+
// Settings page — ports come from health endpoint (d.port / d.mitm_port)
|
|
879
|
+
var httpPort = d.port || window.location.port || '8080';
|
|
880
|
+
var mitmPort = d.mitm_port || (parseInt(String(httpPort)) + 1);
|
|
881
|
+
var setEl = function(id, v){ var e = document.getElementById(id); if(e) e.textContent = v; };
|
|
882
|
+
setEl('cfg-url-val', 'http://localhost:' + httpPort);
|
|
883
|
+
setEl('cfg-oai-val', 'http://localhost:' + httpPort + '/v1');
|
|
884
|
+
setEl('cfg-gem-val', 'http://localhost:' + httpPort);
|
|
885
|
+
setEl('cfg-mitm-val', 'http://localhost:' + mitmPort);
|
|
886
|
+
var ih = document.getElementById('inp-http-port'); if(ih && !ih.value) ih.value = String(httpPort);
|
|
887
|
+
var im = document.getElementById('inp-mitm-port'); if(im && !im.value) im.value = String(mitmPort);
|
|
888
|
+
if (d.version) document.getElementById('cfg-ver').textContent = d.version;
|
|
889
|
+
if (d.uptime_seconds != null) document.getElementById('cfg-uptime').textContent = fmtUptime(d.uptime_seconds);
|
|
890
|
+
document.getElementById('cfg-mode').textContent = mode;
|
|
891
|
+
document.getElementById('cfg-bypass').textContent = byp ? 'enabled' : 'disabled';
|
|
892
|
+
// Backend selector state
|
|
893
|
+
if (d.compression_backend) updateBackendButtons(d.compression_backend);
|
|
894
|
+
// Native compact toggle state
|
|
895
|
+
var ncBtn = document.getElementById('native-compact-btn');
|
|
896
|
+
if (ncBtn) {
|
|
897
|
+
var ncOn = !!d.anthropic_native_compact;
|
|
898
|
+
ncBtn.textContent = ncOn ? 'ON' : 'OFF';
|
|
899
|
+
ncBtn.style.background = ncOn ? 'var(--brand)' : '';
|
|
900
|
+
ncBtn.style.color = ncOn ? 'white' : '';
|
|
901
|
+
}
|
|
902
|
+
if (d.circuit_breaker) {
|
|
903
|
+
var cb = d.circuit_breaker;
|
|
904
|
+
document.getElementById('cfg-cb').textContent = cb.state + (cb.total_trips ? ' · ' + cb.total_trips + ' trips' : '');
|
|
905
|
+
}
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
function renderTools(tools) {
|
|
909
|
+
var el = document.getElementById('tools-body');
|
|
910
|
+
if (!tools || typeof tools !== 'object') {
|
|
911
|
+
el.innerHTML = '<span style="font-size:13px;color:var(--text3)">No tool data yet</span>';
|
|
912
|
+
return;
|
|
913
|
+
}
|
|
914
|
+
// tools can be { ToolName: count } or { ToolName: { count, saved_tokens } }
|
|
915
|
+
var entries = Object.entries(tools)
|
|
916
|
+
.map(function(e){ return [e[0], typeof e[1] === 'object' ? e[1].count || 0 : e[1]]; })
|
|
917
|
+
.filter(function(e){ return e[1] > 0; })
|
|
918
|
+
.sort(function(a,b){ return b[1]-a[1]; })
|
|
919
|
+
.slice(0,6);
|
|
920
|
+
if (!entries.length) {
|
|
921
|
+
el.innerHTML = '<span style="font-size:13px;color:var(--text3)">No tools recorded yet</span>';
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
var max = entries[0][1];
|
|
925
|
+
el.innerHTML = entries.map(function(e){
|
|
926
|
+
var pct = max > 0 ? Math.round(e[1]/max*100) : 0;
|
|
927
|
+
return '<div class="tool-row"><span class="tool-name">'+esc(e[0])+'</span>'+
|
|
928
|
+
'<div class="tool-track"><div class="tool-fill" style="width:'+pct+'%"></div></div>'+
|
|
929
|
+
'<span class="tool-count">'+fmt(e[1])+'</span></div>';
|
|
930
|
+
}).join('');
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
function renderLimits(lim) {
|
|
934
|
+
var el = document.getElementById('limits-body');
|
|
935
|
+
if (!lim || typeof lim !== 'object') {
|
|
936
|
+
el.innerHTML = '<div class="lim-nodata">No limit data yet.</div>';
|
|
937
|
+
return;
|
|
938
|
+
}
|
|
939
|
+
var claudeRows = [];
|
|
940
|
+
var openaiRows = [];
|
|
941
|
+
var rows = claudeRows; // default alias for Claude section
|
|
942
|
+
|
|
943
|
+
// ── Claude / Anthropic ──────────────────────────────────────────────────
|
|
944
|
+
var a = lim.anthropic;
|
|
945
|
+
if (a) {
|
|
946
|
+
// unified = Claude Code Max / subscription plan rate limits
|
|
947
|
+
var u = a.unified;
|
|
948
|
+
if (u && u.hasData) {
|
|
949
|
+
var p5 = Math.round(u.fiveHourUtilization * 100);
|
|
950
|
+
var p7 = Math.round(u.sevenDayUtilization * 100);
|
|
951
|
+
var c5 = p5 > 90 ? 'crit' : p5 > 70 ? 'warn' : 'ok';
|
|
952
|
+
var c7 = p7 > 90 ? 'crit' : p7 > 70 ? 'warn' : 'ok';
|
|
953
|
+
var reset5 = u.fiveHourResetEpoch ? new Date(u.fiveHourResetEpoch).toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'}) : '';
|
|
954
|
+
var reset7 = u.sevenDayResetEpoch ? new Date(u.sevenDayResetEpoch).toLocaleDateString([], {month:'short',day:'numeric'}) : '';
|
|
955
|
+
rows.push(limRow('Claude 5h window', p5, c5, p5 + '% used' + (reset5 ? ' · resets ' + reset5 : '')));
|
|
956
|
+
rows.push(limRow('Claude 7d window', p7, c7, p7 + '% used' + (reset7 ? ' · resets ' + reset7 : '')));
|
|
957
|
+
}
|
|
958
|
+
// rl = standard API rate limit headers (available with API key, not subscription)
|
|
959
|
+
var rl = a.rl;
|
|
960
|
+
if (rl && rl.hasData) {
|
|
961
|
+
if (rl.tokensLimit > 0) {
|
|
962
|
+
var used = rl.tokensLimit - rl.tokensRemaining;
|
|
963
|
+
var pp = Math.round(used / rl.tokensLimit * 100);
|
|
964
|
+
rows.push(limRow('Claude tokens/min', pp, pp > 90 ? 'crit' : pp > 70 ? 'warn' : 'ok',
|
|
965
|
+
fmt(used) + ' / ' + fmt(rl.tokensLimit)));
|
|
966
|
+
}
|
|
967
|
+
if (rl.requestsLimit > 0) {
|
|
968
|
+
var usedR = rl.requestsLimit - rl.requestsRemaining;
|
|
969
|
+
var ppR = Math.round(usedR / rl.requestsLimit * 100);
|
|
970
|
+
rows.push(limRow('Claude req/min', ppR, ppR > 90 ? 'crit' : ppR > 70 ? 'warn' : 'ok',
|
|
971
|
+
usedR + ' / ' + rl.requestsLimit));
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
// usage — actual tokens sent to Anthropic this session
|
|
975
|
+
if (a.usage && (a.usage.inputSession || a.usage.outputSession)) {
|
|
976
|
+
rows.push('<div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border)">' +
|
|
977
|
+
'<div style="font-size:11px;color:var(--text3);margin-bottom:4px">Actual API tokens this session</div>' +
|
|
978
|
+
'<div style="font-size:13px;color:var(--text2)">' +
|
|
979
|
+
'In: <strong style="color:var(--text)">' + fmt(a.usage.inputSession) + '</strong> ' +
|
|
980
|
+
'Out: <strong style="color:var(--text)">' + fmt(a.usage.outputSession) + '</strong>' +
|
|
981
|
+
'</div></div>');
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
// ── OpenAI / Codex ──────────────────────────────────────────────────────
|
|
986
|
+
rows = openaiRows; // switch alias
|
|
987
|
+
var o = lim.openai;
|
|
988
|
+
if (o) {
|
|
989
|
+
var os = o.session;
|
|
990
|
+
if (os && os.hasData && os.primary) {
|
|
991
|
+
var pp2 = os.primary.usedPercent || 0;
|
|
992
|
+
var c2 = pp2 > 90 ? 'crit' : pp2 > 70 ? 'warn' : 'ok';
|
|
993
|
+
var resetTs = os.primary.resetsAt ? new Date(os.primary.resetsAt * 1000).toLocaleDateString([],{month:'short',day:'numeric'}) : '';
|
|
994
|
+
rows.push(limRow('Codex ' + (os.primary.windowDurationMins >= 10080 ? '7d' : os.primary.windowDurationMins + 'min'),
|
|
995
|
+
pp2, c2, pp2 + '% used' + (resetTs ? ' · resets ' + resetTs : '')));
|
|
996
|
+
}
|
|
997
|
+
if (o.usage && o.usage.inputSession) {
|
|
998
|
+
rows.push('<div style="margin-top:10px;padding-top:10px;border-top:1px solid var(--border)">' +
|
|
999
|
+
'<div style="font-size:11px;color:var(--text3);margin-bottom:4px">Codex tokens this session</div>' +
|
|
1000
|
+
'<div style="font-size:13px;color:var(--text2)">' +
|
|
1001
|
+
'In: <strong style="color:var(--text)">' + fmt(o.usage.inputSession) + '</strong> ' +
|
|
1002
|
+
'Out: <strong style="color:var(--text)">' + fmt(o.usage.outputSession) + '</strong>' +
|
|
1003
|
+
'</div></div>');
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
if (!claudeRows.length && !openaiRows.length) {
|
|
1008
|
+
el.innerHTML = '<div class="lim-nodata">No limit data yet — appears after first API call.</div>';
|
|
1009
|
+
return;
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
var col = function(title, content) {
|
|
1013
|
+
return '<div>' +
|
|
1014
|
+
'<div style="font-size:11px;font-weight:600;text-transform:uppercase;letter-spacing:.5px;color:var(--text3);margin-bottom:10px">' + title + '</div>' +
|
|
1015
|
+
(content || '<div class="lim-nodata" style="padding:0">No data</div>') +
|
|
1016
|
+
'</div>';
|
|
1017
|
+
};
|
|
1018
|
+
|
|
1019
|
+
el.innerHTML = '<div style="display:grid;grid-template-columns:1fr 1fr;gap:20px">' +
|
|
1020
|
+
col('Claude', claudeRows.length ? '<div class="limits-grid">' + claudeRows.join('') + '</div>' : null) +
|
|
1021
|
+
col('Codex / OpenAI', openaiRows.length ? '<div class="limits-grid">' + openaiRows.join('') + '</div>' : null) +
|
|
1022
|
+
'</div>';
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
function limRow(name, pct, cls, label) {
|
|
1026
|
+
return '<div class="lim-row">'+
|
|
1027
|
+
'<span class="lim-name">'+esc(name)+'</span>'+
|
|
1028
|
+
'<div class="lim-track"><div class="lim-fill '+cls+'" style="width:'+pct+'%"></div></div>'+
|
|
1029
|
+
'<span class="lim-text">'+esc(label)+'</span></div>';
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// ── Pricing table ($/1M tokens) ───────────────────────────────────────────
|
|
1033
|
+
var PRICING = {
|
|
1034
|
+
// ── Claude (verified May 2026 — claude.com/pricing) ──
|
|
1035
|
+
'claude-opus-4-7': { input: 5, output: 25 }, // current flagship, Apr 2026
|
|
1036
|
+
'claude-opus-4-6': { input: 5, output: 25 },
|
|
1037
|
+
'claude-opus-4-5': { input: 5, output: 25 },
|
|
1038
|
+
'claude-opus-4-1': { input: 15, output: 75 }, // legacy
|
|
1039
|
+
'claude-opus-4': { input: 15, output: 75 }, // legacy
|
|
1040
|
+
'claude-opus-3': { input: 15, output: 75 }, // legacy
|
|
1041
|
+
'claude-sonnet-4-6': { input: 3, output: 15 },
|
|
1042
|
+
'claude-sonnet-4-5': { input: 3, output: 15 },
|
|
1043
|
+
'claude-sonnet-4': { input: 3, output: 15 },
|
|
1044
|
+
'claude-3-7-sonnet': { input: 3, output: 15 },
|
|
1045
|
+
'claude-3-5-sonnet': { input: 3, output: 15 },
|
|
1046
|
+
'claude-3-sonnet': { input: 3, output: 15 },
|
|
1047
|
+
'claude-haiku-4-5': { input: 1, output: 5 }, // current
|
|
1048
|
+
'claude-haiku-3-5': { input: 0.8, output: 4 },
|
|
1049
|
+
'claude-3-5-haiku': { input: 0.8, output: 4 },
|
|
1050
|
+
'claude-3-haiku': { input: 0.25, output: 1.25 },
|
|
1051
|
+
// ── OpenAI (verified May 2026 — openai.com/api/pricing) ──
|
|
1052
|
+
'gpt-5-5-pro': { input: 30, output: 180 }, // research-grade
|
|
1053
|
+
'gpt-5-5': { input: 5, output: 30 }, // current flagship
|
|
1054
|
+
'gpt-5-4-pro': { input: 30, output: 180 },
|
|
1055
|
+
'gpt-5-4': { input: 2.5, output: 15 }, // production workhorse
|
|
1056
|
+
'gpt-5-4-mini': { input: 0.75, output: 4.5 },
|
|
1057
|
+
'gpt-5-4-nano': { input: 0.20, output: 1.25 },
|
|
1058
|
+
'gpt-5-3-codex': { input: 2.5, output: 15 },
|
|
1059
|
+
'gpt-4o': { input: 2.5, output: 10 },
|
|
1060
|
+
'gpt-4o-mini': { input: 0.15, output: 0.6 },
|
|
1061
|
+
'gpt-4-1': { input: 2, output: 8 },
|
|
1062
|
+
'gpt-4-1-mini': { input: 0.40, output: 1.6 },
|
|
1063
|
+
'gpt-4-1-nano': { input: 0.10, output: 0.4 },
|
|
1064
|
+
'gpt-4-turbo': { input: 10, output: 30 },
|
|
1065
|
+
'gpt-4': { input: 30, output: 60 },
|
|
1066
|
+
'o3': { input: 2, output: 8 }, // cut from $10 to $2 in 2026
|
|
1067
|
+
'o3-mini': { input: 1.1, output: 4.4 },
|
|
1068
|
+
'o4-mini': { input: 1.1, output: 4.4 },
|
|
1069
|
+
'o1': { input: 15, output: 60 }, // legacy
|
|
1070
|
+
'o1-mini': { input: 3, output: 12 },
|
|
1071
|
+
'o1-pro': { input: 150, output: 600 },
|
|
1072
|
+
'codex-mini-latest': { input: 1.5, output: 6 },
|
|
1073
|
+
// ── Gemini ──
|
|
1074
|
+
'gemini-2.5-pro': { input: 1.25, output: 10 },
|
|
1075
|
+
'gemini-2.5-flash': { input: 0.075,output: 0.3 },
|
|
1076
|
+
'gemini-2.0-flash': { input: 0.1, output: 0.4 },
|
|
1077
|
+
'gemini-2.0-flash-lite': { input: 0.075,output: 0.3 },
|
|
1078
|
+
'gemini-1.5-pro': { input: 1.25, output: 5 },
|
|
1079
|
+
'gemini-1.5-flash': { input: 0.075,output: 0.3 },
|
|
1080
|
+
};
|
|
1081
|
+
var DEFAULT_PRICE_INPUT = 3; // Claude Sonnet fallback
|
|
1082
|
+
|
|
1083
|
+
function getModelPrice(model) {
|
|
1084
|
+
if (!model) return DEFAULT_PRICE_INPUT;
|
|
1085
|
+
var m = model.toLowerCase();
|
|
1086
|
+
// exact match first
|
|
1087
|
+
if (PRICING[m]) return PRICING[m].input;
|
|
1088
|
+
// prefix match (handles date-stamped variants like claude-sonnet-4-5-20251101)
|
|
1089
|
+
for (var key in PRICING) {
|
|
1090
|
+
if (m.startsWith(key) || m.includes(key)) return PRICING[key].input;
|
|
1091
|
+
}
|
|
1092
|
+
return DEFAULT_PRICE_INPUT;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
function calcCostFromModels(byModel, getOriginal) {
|
|
1096
|
+
// If we have per-model breakdown, weight by actual model price
|
|
1097
|
+
if (!byModel || !Object.keys(byModel).length) return null;
|
|
1098
|
+
var totalCost = 0;
|
|
1099
|
+
var savedCost = 0;
|
|
1100
|
+
for (var model in byModel) {
|
|
1101
|
+
var data = byModel[model];
|
|
1102
|
+
var price = getModelPrice(model) / 1000000; // $/token
|
|
1103
|
+
var origTok = getOriginal ? data.original_tokens : 0;
|
|
1104
|
+
var savedTok = data.saved_tokens || 0;
|
|
1105
|
+
totalCost += origTok * price;
|
|
1106
|
+
savedCost += savedTok * price;
|
|
1107
|
+
}
|
|
1108
|
+
return { totalCost: totalCost, savedCost: savedCost };
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
// ── Model breakdown ────────────────────────────────────────────────────────
|
|
1112
|
+
function renderModelBreakdown(byModel) {
|
|
1113
|
+
var el = document.getElementById('model-body');
|
|
1114
|
+
var elSav = document.getElementById('model-body-savings');
|
|
1115
|
+
var noData = '<span style="font-size:13px;color:var(--text3)">No model data yet — appears after first request.</span>';
|
|
1116
|
+
if (!byModel || !Object.keys(byModel).length) {
|
|
1117
|
+
if (el) el.innerHTML = noData;
|
|
1118
|
+
if (elSav) elSav.innerHTML = noData;
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
var rows = Object.entries(byModel)
|
|
1122
|
+
.filter(function(e){ return e[1].requests > 0; })
|
|
1123
|
+
.sort(function(a,b){ return b[1].saved_tokens - a[1].saved_tokens; });
|
|
1124
|
+
if (!rows.length) { if (el) el.innerHTML = noData; if (elSav) elSav.innerHTML = noData; return; }
|
|
1125
|
+
|
|
1126
|
+
var maxSaved = rows[0][1].saved_tokens || 1;
|
|
1127
|
+
var html = rows.map(function(e){
|
|
1128
|
+
var model = e[0];
|
|
1129
|
+
var data = e[1];
|
|
1130
|
+
var priceIn = getModelPrice(model);
|
|
1131
|
+
var savedCost = data.saved_tokens * priceIn / 1000000;
|
|
1132
|
+
var origCost = data.original_tokens * priceIn / 1000000;
|
|
1133
|
+
var pct = Math.round((data.saved_tokens / maxSaved) * 100);
|
|
1134
|
+
var priceLabel = priceIn === DEFAULT_PRICE_INPUT ? '$' + priceIn + '/1M (est.)' : '$' + priceIn + '/1M input';
|
|
1135
|
+
return '<div style="margin-bottom:14px">' +
|
|
1136
|
+
'<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:4px">' +
|
|
1137
|
+
'<span style="font-size:13px;font-weight:600;color:var(--text);font-family:monospace">' + esc(model) + '</span>' +
|
|
1138
|
+
'<span style="font-size:12px;color:var(--text3)">' + priceLabel + '</span>' +
|
|
1139
|
+
'</div>' +
|
|
1140
|
+
'<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px">' +
|
|
1141
|
+
'<span style="font-size:12px;color:var(--text2)">' + fmt(data.saved_tokens) + ' tokens saved · ' + data.savings_pct + '%</span>' +
|
|
1142
|
+
'<span style="font-size:12px;color:var(--brand2);font-weight:600">' + fmtUsd(savedCost) + ' saved</span>' +
|
|
1143
|
+
'</div>' +
|
|
1144
|
+
'<div style="height:6px;background:var(--surface3);border-radius:3px;overflow:hidden;margin-bottom:3px">' +
|
|
1145
|
+
'<div style="height:100%;width:' + pct + '%;background:var(--brand);border-radius:3px;transition:width .4s"></div>' +
|
|
1146
|
+
'</div>' +
|
|
1147
|
+
'<div style="font-size:11px;color:var(--text3)">' +
|
|
1148
|
+
data.requests + ' req · without Squeezr: ' + fmtUsd(origCost) + ' · with: ' + fmtUsd(origCost - savedCost) +
|
|
1149
|
+
'</div>' +
|
|
1150
|
+
'</div>';
|
|
1151
|
+
}).join('');
|
|
1152
|
+
if (el) el.innerHTML = html;
|
|
1153
|
+
if (elSav) elSav.innerHTML = html;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
// ── Client breakdown (#8) ──────────────────────────────────────────────────
|
|
1157
|
+
var clientOpen = false;
|
|
1158
|
+
var CLIENT_LABELS = {
|
|
1159
|
+
claude_code: 'Claude Code',
|
|
1160
|
+
claude_desktop: 'Claude Desktop',
|
|
1161
|
+
aider: 'Aider',
|
|
1162
|
+
opencode: 'OpenCode',
|
|
1163
|
+
codex_desktop: 'Codex Desktop',
|
|
1164
|
+
cursor: 'Cursor',
|
|
1165
|
+
continue: 'Continue.dev',
|
|
1166
|
+
cline: 'Cline / Roo',
|
|
1167
|
+
windsurf: 'Windsurf',
|
|
1168
|
+
openai_other: 'OpenAI (other)',
|
|
1169
|
+
gemini: 'Gemini CLI',
|
|
1170
|
+
mitm: 'Codex CLI',
|
|
1171
|
+
};
|
|
1172
|
+
|
|
1173
|
+
function setBackend(name) {
|
|
1174
|
+
fetch('/squeezr/backend', {
|
|
1175
|
+
method: 'POST',
|
|
1176
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1177
|
+
body: JSON.stringify({ backend: name }),
|
|
1178
|
+
})
|
|
1179
|
+
.then(function(r){ return r.json(); })
|
|
1180
|
+
.then(function(d){ updateBackendButtons(d.backend); })
|
|
1181
|
+
.catch(function(){});
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
function updateBackendButtons(active) {
|
|
1185
|
+
var btns = document.querySelectorAll('button[data-backend]');
|
|
1186
|
+
for (var i = 0; i < btns.length; i++) {
|
|
1187
|
+
var b = btns[i];
|
|
1188
|
+
var isActive = b.getAttribute('data-backend') === active;
|
|
1189
|
+
b.className = 'mode-btn' + (isActive ? ' active' : '');
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
function toggleNativeCompact() {
|
|
1194
|
+
fetch('/squeezr/native-compact', { method: 'POST' })
|
|
1195
|
+
.then(function(r){ return r.json(); })
|
|
1196
|
+
.then(function(d){
|
|
1197
|
+
var btn = document.getElementById('native-compact-btn');
|
|
1198
|
+
if (btn) {
|
|
1199
|
+
btn.textContent = d.enabled ? 'ON' : 'OFF';
|
|
1200
|
+
btn.style.background = d.enabled ? 'var(--brand)' : '';
|
|
1201
|
+
btn.style.color = d.enabled ? 'white' : '';
|
|
1202
|
+
}
|
|
1203
|
+
})
|
|
1204
|
+
.catch(function(){});
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function toggleClientBreakdown() {
|
|
1208
|
+
clientOpen = !clientOpen;
|
|
1209
|
+
document.getElementById('cli-breakdown').style.display = clientOpen ? '' : 'none';
|
|
1210
|
+
document.getElementById('cli-chevron').style.transform = clientOpen ? 'rotate(180deg)' : '';
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
function renderClientBreakdown(byClient) {
|
|
1214
|
+
var noData = '<span style="font-size:13px;color:var(--text3)">No client data yet — starts after first request.</span>';
|
|
1215
|
+
var elOverview = document.getElementById('client-body-overview');
|
|
1216
|
+
var elSettings = document.getElementById('cli-breakdown-body');
|
|
1217
|
+
|
|
1218
|
+
if (!byClient || !Object.keys(byClient).length) {
|
|
1219
|
+
if (elOverview) elOverview.innerHTML = noData;
|
|
1220
|
+
if (elSettings) elSettings.innerHTML = noData;
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
var rows = Object.entries(byClient)
|
|
1224
|
+
.filter(function(e){ return e[1].requests > 0; })
|
|
1225
|
+
.sort(function(a,b){ return b[1].saved_tokens - a[1].saved_tokens; });
|
|
1226
|
+
if (!rows.length) {
|
|
1227
|
+
if (elOverview) elOverview.innerHTML = noData;
|
|
1228
|
+
if (elSettings) elSettings.innerHTML = noData;
|
|
1229
|
+
return;
|
|
1230
|
+
}
|
|
1231
|
+
var maxSaved = rows[0][1].saved_tokens || 1;
|
|
1232
|
+
var html = rows.map(function(e){
|
|
1233
|
+
var label = CLIENT_LABELS[e[0]] || e[0];
|
|
1234
|
+
var data = e[1];
|
|
1235
|
+
var pct = Math.round((data.saved_tokens / maxSaved) * 100);
|
|
1236
|
+
return '<div style="margin-bottom:12px">' +
|
|
1237
|
+
'<div style="display:flex;justify-content:space-between;align-items:baseline;margin-bottom:5px">' +
|
|
1238
|
+
'<span style="font-size:13px;font-weight:600;color:var(--text)">' + esc(label) + '</span>' +
|
|
1239
|
+
'<span style="font-size:12px;color:var(--brand2);font-weight:600">' + fmt(data.saved_tokens) + ' saved <span style="color:var(--text3);font-weight:400">· ' + data.savings_pct + '%</span></span>' +
|
|
1240
|
+
'</div>' +
|
|
1241
|
+
'<div style="height:6px;background:var(--surface3);border-radius:3px;overflow:hidden;margin-bottom:3px">' +
|
|
1242
|
+
'<div style="height:100%;width:' + pct + '%;background:var(--brand);border-radius:3px;transition:width .4s"></div>' +
|
|
1243
|
+
'</div>' +
|
|
1244
|
+
'<div style="font-size:11px;color:var(--text3)">' + data.requests + ' req · ~' + fmt(data.original_tokens) + ' tokens in</div>' +
|
|
1245
|
+
'</div>';
|
|
1246
|
+
}).join('');
|
|
1247
|
+
if (elOverview) elOverview.innerHTML = html;
|
|
1248
|
+
if (elSettings) elSettings.innerHTML = html;
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function updateMode(mode, byp) {
|
|
1252
|
+
var mb = document.getElementById('mode-badge');
|
|
1253
|
+
mb.textContent = 'mode: ' + mode;
|
|
1254
|
+
mb.className = 'badge' + (mode === 'off' ? ' red' : ' green');
|
|
1255
|
+
|
|
1256
|
+
var bb = document.getElementById('bypass-badge');
|
|
1257
|
+
bb.textContent = byp ? 'bypass: on' : 'bypass: off';
|
|
1258
|
+
bb.className = 'badge' + (byp ? ' yellow' : '');
|
|
1259
|
+
|
|
1260
|
+
document.querySelectorAll('.mode-btn').forEach(function(btn) {
|
|
1261
|
+
btn.className = 'mode-btn' + (btn.dataset.mode === mode ? (mode === 'off' ? ' active-off' : ' active') : '');
|
|
1262
|
+
});
|
|
1263
|
+
document.getElementById('bypass-btn').className = 'bypass-btn' + (byp ? ' active' : '');
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1266
|
+
// ── Controls ───────────────────────────────────────────────────────────────
|
|
1267
|
+
function setMode(mode) {
|
|
1268
|
+
fetch('/squeezr/config', {
|
|
1269
|
+
method:'POST', headers:{'Content-Type':'application/json'},
|
|
1270
|
+
body: JSON.stringify({mode: mode})
|
|
1271
|
+
}).then(function(r){
|
|
1272
|
+
if (r.ok && lastStats) { lastStats.mode = mode; updateMode(mode, !!(lastStats.bypass || lastStats.bypassed)); }
|
|
1273
|
+
});
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
function toggleBypass() {
|
|
1277
|
+
fetch('/squeezr/bypass', {method:'POST'}).then(function(r){ if(r.ok) poll(); });
|
|
1278
|
+
}
|
|
1279
|
+
|
|
1280
|
+
// ── Connection ─────────────────────────────────────────────────────────────
|
|
1281
|
+
var pollTimer = null, sseOk = false;
|
|
1282
|
+
|
|
1283
|
+
function poll() {
|
|
1284
|
+
fetch('/squeezr/stats')
|
|
1285
|
+
.then(function(r){ return r.json(); })
|
|
1286
|
+
.then(function(d){
|
|
1287
|
+
setConn(true);
|
|
1288
|
+
try { render(d); } catch(e){ console.error('[squeezr] render error:', e); }
|
|
1289
|
+
})
|
|
1290
|
+
.catch(function(){ setConn(false); });
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function setConn(ok) {
|
|
1294
|
+
document.getElementById('conn-dot').className = 'conn-dot ' + (ok ? 'online' : 'offline');
|
|
1295
|
+
document.getElementById('conn-label').textContent = ok ? 'Connected' : 'Offline';
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
function startPoll() {
|
|
1299
|
+
if (!pollTimer) { pollTimer = setInterval(poll, 5000); poll(); }
|
|
1300
|
+
}
|
|
1301
|
+
|
|
1302
|
+
function connect() {
|
|
1303
|
+
var es;
|
|
1304
|
+
try { es = new EventSource('/squeezr/events'); }
|
|
1305
|
+
catch(e) { setConn(false); startPoll(); return; }
|
|
1306
|
+
|
|
1307
|
+
var timer = setTimeout(function(){ if (!sseOk){ es.close(); setConn(false); startPoll(); } }, 6000);
|
|
1308
|
+
|
|
1309
|
+
es.onopen = function(){ clearTimeout(timer); sseOk = true; setConn(true); clearInterval(pollTimer); pollTimer = null; };
|
|
1310
|
+
es.onmessage = function(ev){
|
|
1311
|
+
clearTimeout(timer);
|
|
1312
|
+
if (!sseOk){ sseOk = true; setConn(true); clearInterval(pollTimer); pollTimer = null; }
|
|
1313
|
+
try { render(JSON.parse(ev.data)); } catch(e){}
|
|
1314
|
+
};
|
|
1315
|
+
es.addEventListener('stats', function(ev){ try { render(JSON.parse(ev.data)); } catch(e){} });
|
|
1316
|
+
es.onerror = function(){
|
|
1317
|
+
clearTimeout(timer); sseOk = false; es.close(); setConn(false); startPoll();
|
|
1318
|
+
setTimeout(connect, 10000);
|
|
1319
|
+
};
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// ── Actions ────────────────────────────────────────────────────────────────
|
|
1323
|
+
function showResult(id, cls, msg) {
|
|
1324
|
+
var el = document.getElementById('action-result-' + id);
|
|
1325
|
+
if (!el) return;
|
|
1326
|
+
el.className = 'action-result ' + cls;
|
|
1327
|
+
el.textContent = msg;
|
|
1328
|
+
el.style.display = 'block';
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
function runAction(action) {
|
|
1332
|
+
if (action === 'status') {
|
|
1333
|
+
fetch('/squeezr/health').then(function(r) { return r.json(); }).then(function(h) {
|
|
1334
|
+
var msg = 'version: ' + (h.version || '?');
|
|
1335
|
+
if (h.uptime != null) msg += ' | uptime: ' + fmtUptime(h.uptime);
|
|
1336
|
+
if (h.mode) msg += ' | mode: ' + h.mode;
|
|
1337
|
+
showResult('status', 'ok', msg);
|
|
1338
|
+
}).catch(function(e) {
|
|
1339
|
+
showResult('status', 'err', 'Error: ' + e.message);
|
|
1340
|
+
});
|
|
1341
|
+
} else if (action === 'stop') {
|
|
1342
|
+
fetch('/squeezr/control/stop', {method:'POST'}).then(function(r) {
|
|
1343
|
+
if (r.ok) {
|
|
1344
|
+
showResult('stop', 'ok', 'Proxy stopped');
|
|
1345
|
+
} else {
|
|
1346
|
+
showResult('stop', 'err', 'Run in terminal: squeezr stop');
|
|
1347
|
+
}
|
|
1348
|
+
}).catch(function() {
|
|
1349
|
+
showResult('stop', 'err', 'Run in terminal: squeezr stop');
|
|
1350
|
+
});
|
|
1351
|
+
} else if (action === 'update') {
|
|
1352
|
+
showResult('update', 'ok', 'Run in terminal: squeezr update');
|
|
1353
|
+
} else if (action === 'ports') {
|
|
1354
|
+
var httpVal = document.getElementById('inp-http-port').value.trim();
|
|
1355
|
+
var mitmVal = document.getElementById('inp-mitm-port').value.trim();
|
|
1356
|
+
var httpN = parseInt(httpVal);
|
|
1357
|
+
var mitmN = parseInt(mitmVal);
|
|
1358
|
+
if (!httpN || httpN < 1024 || httpN > 65535 || !mitmN || mitmN < 1024 || mitmN > 65535) {
|
|
1359
|
+
showResult('ports', 'err', 'Invalid ports — must be between 1024 and 65535');
|
|
1360
|
+
return;
|
|
1361
|
+
}
|
|
1362
|
+
if (httpN === mitmN) {
|
|
1363
|
+
showResult('ports', 'err', 'HTTP and MITM ports must be different');
|
|
1364
|
+
return;
|
|
1365
|
+
}
|
|
1366
|
+
fetch('/squeezr/ports', {
|
|
1367
|
+
method: 'POST',
|
|
1368
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1369
|
+
body: JSON.stringify({ port: httpN, mitm_port: mitmN })
|
|
1370
|
+
}).then(function(r) {
|
|
1371
|
+
if (r.ok) {
|
|
1372
|
+
showResult('ports', 'ok', 'Ports saved to squeezr.toml — restart Squeezr to apply (squeezr stop && squeezr start)');
|
|
1373
|
+
} else {
|
|
1374
|
+
r.text().then(function(t) { showResult('ports', 'err', 'Failed: ' + t); });
|
|
1375
|
+
}
|
|
1376
|
+
}).catch(function(e) { showResult('ports', 'err', e.message); });
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
// ── Version check ──────────────────────────────────────────────────────────
|
|
1381
|
+
// Returns 1 if a > b, -1 if a < b, 0 if equal. Compares numeric major.minor.patch;
|
|
1382
|
+
// any non-numeric prerelease tail makes the version "lower" (so 1.46.0 > 1.46.0-rc.1).
|
|
1383
|
+
function compareSemver(a, b) {
|
|
1384
|
+
function parse(v) {
|
|
1385
|
+
var m = String(v || '').match(/^(\d+)\.(\d+)\.(\d+)(?:-(.+))?/);
|
|
1386
|
+
if (!m) return [0, 0, 0, ''];
|
|
1387
|
+
return [parseInt(m[1], 10), parseInt(m[2], 10), parseInt(m[3], 10), m[4] || ''];
|
|
1388
|
+
}
|
|
1389
|
+
var pa = parse(a), pb = parse(b);
|
|
1390
|
+
for (var i = 0; i < 3; i++) {
|
|
1391
|
+
if (pa[i] > pb[i]) return 1;
|
|
1392
|
+
if (pa[i] < pb[i]) return -1;
|
|
1393
|
+
}
|
|
1394
|
+
if (pa[3] && !pb[3]) return -1;
|
|
1395
|
+
if (!pa[3] && pb[3]) return 1;
|
|
1396
|
+
return 0;
|
|
1397
|
+
}
|
|
1398
|
+
|
|
1399
|
+
function checkLatestVersion() {
|
|
1400
|
+
fetch('/squeezr/health').then(function(r) { return r.json(); }).then(function(h) {
|
|
1401
|
+
var current = h.version;
|
|
1402
|
+
fetch('https://registry.npmjs.org/squeezr-ai/latest')
|
|
1403
|
+
.then(function(r) { return r.json(); }).then(function(npm) {
|
|
1404
|
+
var latest = npm.version;
|
|
1405
|
+
if (latest && current && compareSemver(latest, current) > 0) {
|
|
1406
|
+
var banner = document.getElementById('update-banner');
|
|
1407
|
+
document.getElementById('update-text').textContent = 'v' + current + ' → v' + latest;
|
|
1408
|
+
banner.style.display = 'flex';
|
|
1409
|
+
}
|
|
1410
|
+
}).catch(function(){});
|
|
1411
|
+
}).catch(function(){});
|
|
1412
|
+
}
|
|
1413
|
+
|
|
1414
|
+
// ── Savings page ──────────────────────────────────────────────────────────
|
|
1415
|
+
var savingsPeriod = 'day';
|
|
1416
|
+
var savingsOffset = 0; // 0 = current period, -1 = previous, +1 = next
|
|
1417
|
+
var savingsCache = null;
|
|
1418
|
+
|
|
1419
|
+
function setSavingsPeriod(p) {
|
|
1420
|
+
savingsPeriod = p;
|
|
1421
|
+
savingsOffset = 0; // reset to current when changing scale
|
|
1422
|
+
['day','week','month','all'].forEach(function(k){
|
|
1423
|
+
document.getElementById('period-' + k).className = 'mode-btn' + (k === p ? ' active' : '');
|
|
1424
|
+
});
|
|
1425
|
+
if (savingsCache) renderSavingsData(savingsCache);
|
|
1426
|
+
}
|
|
1427
|
+
|
|
1428
|
+
function navigatePeriod(dir) {
|
|
1429
|
+
if (dir === 0) { savingsOffset = 0; }
|
|
1430
|
+
else { savingsOffset += dir; if (savingsOffset > 0) savingsOffset = 0; }
|
|
1431
|
+
if (savingsCache) renderSavingsData(savingsCache);
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
// Get [start, end] timestamps for the selected period+offset
|
|
1435
|
+
function getPeriodRange() {
|
|
1436
|
+
var now = new Date();
|
|
1437
|
+
var start, end, label;
|
|
1438
|
+
if (savingsPeriod === 'day') {
|
|
1439
|
+
var d = new Date(now); d.setDate(d.getDate() + savingsOffset);
|
|
1440
|
+
start = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
1441
|
+
end = start + 86400000;
|
|
1442
|
+
label = d.toLocaleDateString([], {weekday:'short', day:'numeric', month:'short'});
|
|
1443
|
+
if (savingsOffset === 0) label = 'Today · ' + label;
|
|
1444
|
+
else if (savingsOffset === -1) label = 'Yesterday · ' + label;
|
|
1445
|
+
} else if (savingsPeriod === 'week') {
|
|
1446
|
+
var d = new Date(now); d.setDate(d.getDate() - d.getDay() + (savingsOffset * 7));
|
|
1447
|
+
start = new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
|
|
1448
|
+
end = start + 7 * 86400000;
|
|
1449
|
+
var endDate = new Date(end - 1);
|
|
1450
|
+
label = new Date(start).toLocaleDateString([], {day:'numeric', month:'short'}) + ' – ' + endDate.toLocaleDateString([], {day:'numeric', month:'short'});
|
|
1451
|
+
} else if (savingsPeriod === 'month') {
|
|
1452
|
+
var d = new Date(now.getFullYear(), now.getMonth() + savingsOffset, 1);
|
|
1453
|
+
start = d.getTime();
|
|
1454
|
+
end = new Date(d.getFullYear(), d.getMonth() + 1, 1).getTime();
|
|
1455
|
+
label = d.toLocaleDateString([], {month:'long', year:'numeric'});
|
|
1456
|
+
} else { // all
|
|
1457
|
+
start = 0;
|
|
1458
|
+
end = Number.MAX_SAFE_INTEGER;
|
|
1459
|
+
label = 'All time';
|
|
1460
|
+
}
|
|
1461
|
+
return { start: start, end: end, label: label };
|
|
1462
|
+
}
|
|
1463
|
+
|
|
1464
|
+
function loadSavings() {
|
|
1465
|
+
fetch('/squeezr/history').then(function(r){ return r.json(); }).then(function(d){
|
|
1466
|
+
savingsCache = d;
|
|
1467
|
+
renderSavingsData(d);
|
|
1468
|
+
}).catch(function(){
|
|
1469
|
+
document.getElementById('savings-chart').innerHTML = '<div class="lim-nodata">Could not load history.</div>';
|
|
1470
|
+
});
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function renderSavingsData(d) {
|
|
1474
|
+
var sessions = (d.sessions || []).slice();
|
|
1475
|
+
if (d.current && d.current.requests > 0) sessions.push(d.current);
|
|
1476
|
+
var range = getPeriodRange();
|
|
1477
|
+
var labelEl = document.getElementById('period-label');
|
|
1478
|
+
if (labelEl) labelEl.textContent = range.label;
|
|
1479
|
+
var filtered = sessions.filter(function(s){ return s.startTime >= range.start && s.startTime < range.end && s.savedTokens != null; });
|
|
1480
|
+
|
|
1481
|
+
// Hero stats
|
|
1482
|
+
var totalSaved = 0, totalReqs = 0, totalOrig = 0, hasOrigData = true;
|
|
1483
|
+
filtered.forEach(function(s){
|
|
1484
|
+
totalSaved += s.savedTokens||0;
|
|
1485
|
+
totalReqs += s.requests||0;
|
|
1486
|
+
if (s.originalChars) {
|
|
1487
|
+
totalOrig += Math.round(s.originalChars / 3.5);
|
|
1488
|
+
} else {
|
|
1489
|
+
hasOrigData = false; // at least one session without original data
|
|
1490
|
+
}
|
|
1491
|
+
});
|
|
1492
|
+
var avgPct = totalOrig > 0 ? Math.round(totalSaved / (totalSaved + (totalOrig - totalSaved)) * 100) : 0;
|
|
1493
|
+
|
|
1494
|
+
// Cost: use model-weighted if available from current stats, else flat $3/1M
|
|
1495
|
+
var svModelCosts = (lastStats && lastStats.by_model) ? calcCostFromModels(lastStats.by_model, true) : null;
|
|
1496
|
+
var svCost, svCostNote;
|
|
1497
|
+
if (svModelCosts && svModelCosts.savedCost > 0) {
|
|
1498
|
+
// Scale model costs proportionally if period ≠ all-time
|
|
1499
|
+
var allSaved = 0;
|
|
1500
|
+
sessions.forEach(function(s){ allSaved += s.savedTokens||0; });
|
|
1501
|
+
var scale = allSaved > 0 ? totalSaved / allSaved : 1;
|
|
1502
|
+
svCost = svModelCosts.savedCost * scale;
|
|
1503
|
+
svCostNote = 'model-weighted pricing';
|
|
1504
|
+
} else {
|
|
1505
|
+
svCost = totalSaved * 0.000003;
|
|
1506
|
+
svCostNote = 'est. at $3/1M tokens';
|
|
1507
|
+
}
|
|
1508
|
+
document.getElementById('sv-tokens').textContent = fmt(totalSaved);
|
|
1509
|
+
document.getElementById('sv-tokens-sub').textContent = hasOrigData && totalOrig > 0
|
|
1510
|
+
? 'of ~' + fmt(totalOrig) + ' processed'
|
|
1511
|
+
: 'tokens saved';
|
|
1512
|
+
document.getElementById('sv-cost').textContent = fmtUsd(svCost);
|
|
1513
|
+
var svCostNoteEl = document.getElementById('sv-cost-note'); if(svCostNoteEl) svCostNoteEl.textContent = svCostNote;
|
|
1514
|
+
document.getElementById('sv-sessions').textContent = String(filtered.length);
|
|
1515
|
+
document.getElementById('sv-requests').textContent = totalReqs + ' requests';
|
|
1516
|
+
document.getElementById('sv-pct').textContent = avgPct > 0 ? avgPct + '%' : '—';
|
|
1517
|
+
|
|
1518
|
+
// Chart title
|
|
1519
|
+
var titles = { day: 'Today (by session)', week: 'Last 7 days', month: 'Last 30 days', all: 'All time' };
|
|
1520
|
+
document.getElementById('savings-chart-title').textContent = titles[savingsPeriod] || '';
|
|
1521
|
+
|
|
1522
|
+
// Bar chart: group by day for week/month/all, by session for day
|
|
1523
|
+
renderSavingsChart(filtered, savingsPeriod);
|
|
1524
|
+
|
|
1525
|
+
// Client breakdown mirrors overview
|
|
1526
|
+
var cli = document.getElementById('client-body-savings');
|
|
1527
|
+
var ovCli = document.getElementById('client-body-overview');
|
|
1528
|
+
if (ovCli) cli.innerHTML = ovCli.innerHTML;
|
|
1529
|
+
}
|
|
1530
|
+
|
|
1531
|
+
function fmtY(v) {
|
|
1532
|
+
if (v >= 1000000) return (v/1000000).toFixed(1).replace(/\.0$/,'') + 'M';
|
|
1533
|
+
if (v >= 1000) return Math.round(v/1000) + 'k';
|
|
1534
|
+
return String(Math.round(v));
|
|
1535
|
+
}
|
|
1536
|
+
|
|
1537
|
+
function renderSavingsChart(sessions, period) {
|
|
1538
|
+
var el = document.getElementById('savings-chart');
|
|
1539
|
+
|
|
1540
|
+
// Build empty buckets covering the full range (even if no sessions)
|
|
1541
|
+
var range = getPeriodRange();
|
|
1542
|
+
var entries = [];
|
|
1543
|
+
if (period === 'day') {
|
|
1544
|
+
// 5-hour windows (matches Claude Code's 5h rate limit window)
|
|
1545
|
+
var windows = [
|
|
1546
|
+
{ start: 0, end: 5, label: '00–05' },
|
|
1547
|
+
{ start: 5, end: 10, label: '05–10' },
|
|
1548
|
+
{ start: 10, end: 15, label: '10–15' },
|
|
1549
|
+
{ start: 15, end: 20, label: '15–20' },
|
|
1550
|
+
{ start: 20, end: 24, label: '20–24' },
|
|
1551
|
+
];
|
|
1552
|
+
windows.forEach(function(w, i) {
|
|
1553
|
+
var t = range.start + w.start * 3600000;
|
|
1554
|
+
entries.push({ key: i, saved: 0, reqs: 0, label: w.label, start: t, end: range.start + w.end * 3600000 });
|
|
1555
|
+
});
|
|
1556
|
+
} else if (period === 'week') {
|
|
1557
|
+
// 7 days
|
|
1558
|
+
for (var i = 0; i < 7; i++) {
|
|
1559
|
+
var t = range.start + i * 86400000;
|
|
1560
|
+
var d = new Date(t);
|
|
1561
|
+
entries.push({ key: i, saved: 0, reqs: 0, label: d.toLocaleDateString([], {weekday:'short', day:'numeric'}), start: t, end: t + 86400000 });
|
|
1562
|
+
}
|
|
1563
|
+
} else if (period === 'month') {
|
|
1564
|
+
// All days in the month
|
|
1565
|
+
var startD = new Date(range.start);
|
|
1566
|
+
var endD = new Date(range.end);
|
|
1567
|
+
var cursor = new Date(startD);
|
|
1568
|
+
while (cursor < endD) {
|
|
1569
|
+
var t = cursor.getTime();
|
|
1570
|
+
var nextDay = new Date(cursor); nextDay.setDate(nextDay.getDate() + 1);
|
|
1571
|
+
entries.push({ key: cursor.getDate(), saved: 0, reqs: 0, label: String(cursor.getDate()), start: t, end: nextDay.getTime() });
|
|
1572
|
+
cursor = nextDay;
|
|
1573
|
+
}
|
|
1574
|
+
} else { // all time → group by month, span from first session to current
|
|
1575
|
+
var minTs = sessions.length ? Math.min.apply(null, sessions.map(function(s){return s.startTime;})) : Date.now();
|
|
1576
|
+
var startD = new Date(new Date(minTs).getFullYear(), new Date(minTs).getMonth(), 1);
|
|
1577
|
+
var endD = new Date();
|
|
1578
|
+
var cursor = new Date(startD);
|
|
1579
|
+
while (cursor <= endD) {
|
|
1580
|
+
var t = cursor.getTime();
|
|
1581
|
+
var nextMonth = new Date(cursor.getFullYear(), cursor.getMonth() + 1, 1);
|
|
1582
|
+
entries.push({ key: cursor.getFullYear() + '-' + cursor.getMonth(), saved: 0, reqs: 0, label: cursor.toLocaleDateString([], {month:'short', year:'2-digit'}), start: t, end: nextMonth.getTime() });
|
|
1583
|
+
cursor = nextMonth;
|
|
1584
|
+
}
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1587
|
+
// Fill buckets with session data
|
|
1588
|
+
sessions.forEach(function(s) {
|
|
1589
|
+
for (var i = 0; i < entries.length; i++) {
|
|
1590
|
+
if (s.startTime >= entries[i].start && s.startTime < entries[i].end) {
|
|
1591
|
+
entries[i].saved += s.savedTokens || 0;
|
|
1592
|
+
entries[i].reqs += s.requests || 0;
|
|
1593
|
+
break;
|
|
1594
|
+
}
|
|
1595
|
+
}
|
|
1596
|
+
});
|
|
1597
|
+
|
|
1598
|
+
var maxVal = Math.max.apply(null, entries.map(function(e){ return e.saved; })) || 1;
|
|
1599
|
+
var totalSaved = entries.reduce(function(a,e){ return a + e.saved; }, 0);
|
|
1600
|
+
var totalReqs = entries.reduce(function(a,e){ return a + e.reqs; }, 0);
|
|
1601
|
+
var n = entries.length;
|
|
1602
|
+
var chartH = 110;
|
|
1603
|
+
|
|
1604
|
+
if (totalSaved === 0 && totalReqs === 0) {
|
|
1605
|
+
el.innerHTML = '<div class="lim-nodata">No data in this period.</div>';
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// Y-axis ticks
|
|
1610
|
+
var yTicks = [0.25, 0.5, 0.75, 1.0];
|
|
1611
|
+
|
|
1612
|
+
// Build grid + bars as nested HTML
|
|
1613
|
+
var gridLines = yTicks.map(function(t){
|
|
1614
|
+
var pct = (1 - t) * 100;
|
|
1615
|
+
return '<div style="position:absolute;left:38px;right:0;top:' + pct + '%;height:1px;border-top:1px dashed var(--border);pointer-events:none">' +
|
|
1616
|
+
'<span style="position:absolute;right:calc(100% + 4px);transform:translateY(-50%);font-size:9px;color:var(--text3);white-space:nowrap">' + fmtY(t * maxVal) + '</span>' +
|
|
1617
|
+
'</div>';
|
|
1618
|
+
}).join('');
|
|
1619
|
+
|
|
1620
|
+
// Baseline
|
|
1621
|
+
gridLines += '<div style="position:absolute;left:38px;right:0;bottom:0;height:1px;background:var(--border2)"></div>';
|
|
1622
|
+
|
|
1623
|
+
var bars = entries.map(function(data) {
|
|
1624
|
+
var ratio = data.saved / maxVal;
|
|
1625
|
+
var hPct = Math.max(2, Math.round(ratio * 100));
|
|
1626
|
+
var isMax = data.saved === maxVal;
|
|
1627
|
+
var color = isMax ? 'var(--brand)' : 'rgba(22,163,74,0.35)';
|
|
1628
|
+
var tip = data.label + ' · ' + fmt(data.saved) + ' tokens saved · ' + data.reqs + ' req';
|
|
1629
|
+
return '<div style="flex:1;min-width:0;display:flex;flex-direction:column;align-items:center;justify-content:flex-end;gap:0;height:' + chartH + 'px;position:relative" title="' + esc(tip) + '">' +
|
|
1630
|
+
(isMax ? '<div style="font-size:9px;color:var(--brand2);font-weight:700;margin-bottom:2px;white-space:nowrap">' + fmtY(data.saved) + '</div>' : '') +
|
|
1631
|
+
'<div class="bar" style="width:calc(100% - 2px);height:' + hPct + '%;background:' + color + ';border-radius:3px 3px 0 0;transition:background .15s,opacity .15s"></div>' +
|
|
1632
|
+
'</div>';
|
|
1633
|
+
}).join('');
|
|
1634
|
+
|
|
1635
|
+
// Smart label spacing: show every N-th label to avoid clutter
|
|
1636
|
+
var labelEvery = n <= 12 ? 1 : n <= 24 ? 2 : n <= 31 ? 5 : Math.ceil(n / 12);
|
|
1637
|
+
var labels = entries.map(function(data, i) {
|
|
1638
|
+
var show = i % labelEvery === 0 || i === n - 1;
|
|
1639
|
+
var content = show ? esc(data.label) : '';
|
|
1640
|
+
return '<div style="flex:1;min-width:0;text-align:center;font-size:9px;color:var(--text3);overflow:hidden;text-overflow:ellipsis;white-space:nowrap;padding:0 1px">' + content + '</div>';
|
|
1641
|
+
}).join('');
|
|
1642
|
+
|
|
1643
|
+
var unitLabel = period === 'day' ? '5h windows' : period === 'week' || period === 'month' ? 'days' : 'months';
|
|
1644
|
+
|
|
1645
|
+
el.innerHTML =
|
|
1646
|
+
'<div class="chart-wrap" style="padding:12px 12px 8px">' +
|
|
1647
|
+
'<div style="position:relative;height:' + chartH + 'px;margin-left:38px">' +
|
|
1648
|
+
gridLines +
|
|
1649
|
+
'<div style="position:absolute;inset:0;display:flex;gap:3px;align-items:flex-end">' + bars + '</div>' +
|
|
1650
|
+
'</div>' +
|
|
1651
|
+
'<div style="display:flex;gap:3px;margin-left:38px;margin-top:4px">' + labels + '</div>' +
|
|
1652
|
+
'<div style="margin-top:6px;font-size:11px;color:var(--text3);display:flex;justify-content:space-between">' +
|
|
1653
|
+
'<span>' + totalReqs + ' request' + (totalReqs !== 1 ? 's' : '') + ' across ' + n + ' ' + unitLabel + '</span>' +
|
|
1654
|
+
'<span style="color:var(--brand2);font-weight:600">' + fmtY(totalSaved) + ' tokens saved</span>' +
|
|
1655
|
+
'</div>' +
|
|
1656
|
+
'</div>';
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
poll();
|
|
1660
|
+
connect();
|
|
1661
|
+
checkLatestVersion();
|
|
1662
|
+
</script>
|
|
1663
|
+
</body>
|
|
1661
1664
|
</html>`;
|