claude-code-templates 1.8.0 → 1.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1939 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Claude Code Analytics - Terminal</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@3.9.1/dist/chart.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Monaco', 'Menlo', 'Ubuntu Mono', monospace;
17
+ background: #0d1117;
18
+ color: #c9d1d9;
19
+ min-height: 100vh;
20
+ line-height: 1.4;
21
+ }
22
+
23
+ .terminal {
24
+ max-width: 1400px;
25
+ margin: 0 auto;
26
+ padding: 20px;
27
+ }
28
+
29
+ .terminal-header {
30
+ border-bottom: 1px solid #30363d;
31
+ padding-bottom: 20px;
32
+ margin-bottom: 20px;
33
+ position: relative;
34
+ }
35
+
36
+ .terminal-title {
37
+ color: #d57455;
38
+ font-size: 1.25rem;
39
+ font-weight: normal;
40
+ display: flex;
41
+ align-items: center;
42
+ gap: 8px;
43
+ }
44
+
45
+ .status-dot {
46
+ width: 8px;
47
+ height: 8px;
48
+ border-radius: 50%;
49
+ background: #3fb950;
50
+ animation: pulse 2s infinite;
51
+ }
52
+
53
+ @keyframes pulse {
54
+ 0%, 100% { opacity: 1; }
55
+ 50% { opacity: 0.6; }
56
+ }
57
+
58
+ .terminal-subtitle {
59
+ color: #7d8590;
60
+ font-size: 0.875rem;
61
+ margin-top: 4px;
62
+ }
63
+
64
+ .github-star-btn {
65
+ position: absolute;
66
+ top: 0;
67
+ right: 0;
68
+ background: #21262d;
69
+ border: 1px solid #30363d;
70
+ color: #c9d1d9;
71
+ padding: 8px 12px;
72
+ border-radius: 6px;
73
+ text-decoration: none;
74
+ font-family: inherit;
75
+ font-size: 0.875rem;
76
+ display: flex;
77
+ align-items: center;
78
+ gap: 6px;
79
+ transition: all 0.2s ease;
80
+ cursor: pointer;
81
+ }
82
+
83
+ .github-star-btn:hover {
84
+ border-color: #d57455;
85
+ background: #30363d;
86
+ color: #d57455;
87
+ text-decoration: none;
88
+ }
89
+
90
+ .github-star-btn .star-icon {
91
+ font-size: 0.75rem;
92
+ }
93
+
94
+ .stats-bar {
95
+ display: flex;
96
+ gap: 40px;
97
+ margin: 20px 0;
98
+ flex-wrap: wrap;
99
+ }
100
+
101
+ .stat {
102
+ display: flex;
103
+ align-items: center;
104
+ gap: 8px;
105
+ }
106
+
107
+ .stat-label {
108
+ color: #7d8590;
109
+ font-size: 0.875rem;
110
+ }
111
+
112
+ .stat-value {
113
+ color: #d57455;
114
+ font-weight: bold;
115
+ }
116
+
117
+ .stat-sublabel {
118
+ color: #7d8590;
119
+ font-size: 0.75rem;
120
+ display: block;
121
+ margin-top: 2px;
122
+ }
123
+
124
+ .chart-controls {
125
+ display: flex;
126
+ align-items: center;
127
+ justify-content: space-between;
128
+ gap: 16px;
129
+ margin: 20px 0;
130
+ padding: 12px 0;
131
+ border-top: 1px solid #21262d;
132
+ border-bottom: 1px solid #21262d;
133
+ }
134
+
135
+ .chart-controls-left {
136
+ display: flex;
137
+ align-items: center;
138
+ gap: 16px;
139
+ }
140
+
141
+ .chart-controls-right {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 12px;
145
+ }
146
+
147
+ .date-control {
148
+ display: flex;
149
+ align-items: center;
150
+ gap: 8px;
151
+ }
152
+
153
+ .date-label {
154
+ color: #7d8590;
155
+ font-size: 0.875rem;
156
+ }
157
+
158
+ .date-input {
159
+ background: #21262d;
160
+ border: 1px solid #30363d;
161
+ color: #c9d1d9;
162
+ padding: 6px 12px;
163
+ border-radius: 4px;
164
+ font-family: inherit;
165
+ font-size: 0.875rem;
166
+ cursor: pointer;
167
+ }
168
+
169
+ .date-input:focus {
170
+ outline: none;
171
+ border-color: #d57455;
172
+ }
173
+
174
+ .refresh-btn {
175
+ background: none;
176
+ border: 1px solid #30363d;
177
+ color: #7d8590;
178
+ padding: 6px 12px;
179
+ border-radius: 4px;
180
+ cursor: pointer;
181
+ font-family: inherit;
182
+ font-size: 0.875rem;
183
+ transition: all 0.2s ease;
184
+ display: flex;
185
+ align-items: center;
186
+ gap: 6px;
187
+ }
188
+
189
+ .refresh-btn:hover {
190
+ border-color: #d57455;
191
+ color: #d57455;
192
+ }
193
+
194
+ .refresh-btn.loading {
195
+ opacity: 0.6;
196
+ cursor: not-allowed;
197
+ }
198
+
199
+ .charts-container {
200
+ display: grid;
201
+ grid-template-columns: 2fr 1fr;
202
+ gap: 30px;
203
+ margin: 20px 0 30px 0;
204
+ }
205
+
206
+ .chart-card {
207
+ background: #161b22;
208
+ border: 1px solid #30363d;
209
+ border-radius: 6px;
210
+ padding: 20px;
211
+ position: relative;
212
+ }
213
+
214
+ .chart-title {
215
+ color: #d57455;
216
+ font-size: 0.875rem;
217
+ text-transform: uppercase;
218
+ margin-bottom: 16px;
219
+ display: flex;
220
+ align-items: center;
221
+ gap: 8px;
222
+ }
223
+
224
+ .chart-canvas {
225
+ width: 100% !important;
226
+ height: 200px !important;
227
+ }
228
+
229
+ .filter-bar {
230
+ display: flex;
231
+ align-items: center;
232
+ gap: 16px;
233
+ margin: 20px 0;
234
+ padding: 12px 0;
235
+ border-top: 1px solid #21262d;
236
+ border-bottom: 1px solid #21262d;
237
+ }
238
+
239
+ .filter-label {
240
+ color: #7d8590;
241
+ font-size: 0.875rem;
242
+ }
243
+
244
+ .filter-buttons {
245
+ display: flex;
246
+ gap: 8px;
247
+ }
248
+
249
+ .filter-btn {
250
+ background: none;
251
+ border: 1px solid #30363d;
252
+ color: #7d8590;
253
+ padding: 4px 12px;
254
+ border-radius: 4px;
255
+ cursor: pointer;
256
+ font-family: inherit;
257
+ font-size: 0.875rem;
258
+ transition: all 0.2s ease;
259
+ }
260
+
261
+ .filter-btn:hover {
262
+ border-color: #d57455;
263
+ color: #d57455;
264
+ }
265
+
266
+ .filter-btn.active {
267
+ background: #d57455;
268
+ border-color: #d57455;
269
+ color: #0d1117;
270
+ }
271
+
272
+ .sessions-table {
273
+ width: 100%;
274
+ border-collapse: collapse;
275
+ }
276
+
277
+ .sessions-table th {
278
+ text-align: left;
279
+ padding: 8px 12px;
280
+ color: #7d8590;
281
+ font-size: 0.875rem;
282
+ font-weight: normal;
283
+ border-bottom: 1px solid #30363d;
284
+ }
285
+
286
+ .sessions-table td {
287
+ padding: 8px 12px;
288
+ font-size: 0.875rem;
289
+ border-bottom: 1px solid #21262d;
290
+ }
291
+
292
+ .sessions-table tr:hover {
293
+ background: #161b22;
294
+ }
295
+
296
+ .session-id {
297
+ color: #d57455;
298
+ font-family: monospace;
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 6px;
302
+ }
303
+
304
+ .process-indicator {
305
+ display: inline-block;
306
+ width: 6px;
307
+ height: 6px;
308
+ background: #3fb950;
309
+ border-radius: 50%;
310
+ animation: pulse 2s infinite;
311
+ cursor: help;
312
+ }
313
+
314
+ .process-indicator.orphan {
315
+ background: #f85149;
316
+ }
317
+
318
+ .session-id-container {
319
+ display: flex;
320
+ flex-direction: column;
321
+ gap: 4px;
322
+ }
323
+
324
+ .session-project {
325
+ color: #c9d1d9;
326
+ }
327
+
328
+ .session-model {
329
+ color: #a5d6ff;
330
+ font-size: 0.8rem;
331
+ max-width: 150px;
332
+ white-space: nowrap;
333
+ overflow: hidden;
334
+ text-overflow: ellipsis;
335
+ }
336
+
337
+ .session-messages {
338
+ color: #7d8590;
339
+ }
340
+
341
+ .session-tokens {
342
+ color: #f85149;
343
+ }
344
+
345
+ .session-time {
346
+ color: #7d8590;
347
+ font-size: 0.8rem;
348
+ }
349
+
350
+ .status-active {
351
+ color: #3fb950;
352
+ font-weight: bold;
353
+ }
354
+
355
+ .status-recent {
356
+ color: #d29922;
357
+ }
358
+
359
+ .status-inactive {
360
+ color: #7d8590;
361
+ }
362
+
363
+ .conversation-state {
364
+ color: #d57455;
365
+ font-style: italic;
366
+ font-size: 0.8rem;
367
+ }
368
+
369
+ .conversation-state.working {
370
+ animation: working-pulse 1.5s infinite;
371
+ }
372
+
373
+ .conversation-state.typing {
374
+ animation: typing-pulse 1.5s infinite;
375
+ }
376
+
377
+ @keyframes working-pulse {
378
+ 0%, 100% { opacity: 1; }
379
+ 50% { opacity: 0.7; }
380
+ }
381
+
382
+ @keyframes typing-pulse {
383
+ 0%, 100% { opacity: 1; }
384
+ 50% { opacity: 0.6; }
385
+ }
386
+
387
+ .status-squares {
388
+ display: flex;
389
+ gap: 2px;
390
+ align-items: center;
391
+ flex-wrap: wrap;
392
+ margin: 0;
393
+ padding: 0;
394
+ }
395
+
396
+ .status-square {
397
+ width: 10px !important;
398
+ height: 10px !important;
399
+ min-width: 10px !important;
400
+ min-height: 10px !important;
401
+ max-width: 10px !important;
402
+ max-height: 10px !important;
403
+ border-radius: 2px;
404
+ cursor: help;
405
+ position: relative;
406
+ flex-shrink: 0;
407
+ box-sizing: border-box;
408
+ }
409
+
410
+ .status-square.success {
411
+ background: #d57455;
412
+ }
413
+
414
+ .status-square.tool {
415
+ background: #f97316;
416
+ }
417
+
418
+ .status-square.error {
419
+ background: #dc2626;
420
+ }
421
+
422
+ .status-square.pending {
423
+ background: #6b7280;
424
+ }
425
+
426
+ /* Additional specificity to override any table styling */
427
+ .sessions-table .status-squares .status-square {
428
+ width: 10px !important;
429
+ height: 10px !important;
430
+ min-width: 10px !important;
431
+ min-height: 10px !important;
432
+ max-width: 10px !important;
433
+ max-height: 10px !important;
434
+ display: inline-block !important;
435
+ font-size: 0 !important;
436
+ line-height: 0 !important;
437
+ border: none !important;
438
+ outline: none !important;
439
+ vertical-align: top !important;
440
+ }
441
+
442
+ .status-square:hover::after {
443
+ content: attr(data-tooltip);
444
+ position: absolute;
445
+ bottom: 100%;
446
+ left: 50%;
447
+ transform: translateX(-50%);
448
+ background: #1c1c1c;
449
+ color: #fff;
450
+ padding: 4px 8px;
451
+ border-radius: 4px;
452
+ font-size: 0.75rem;
453
+ white-space: nowrap;
454
+ z-index: 1000;
455
+ margin-bottom: 4px;
456
+ border: 1px solid #30363d;
457
+ }
458
+
459
+ .status-square:hover::before {
460
+ content: '';
461
+ position: absolute;
462
+ bottom: 100%;
463
+ left: 50%;
464
+ transform: translateX(-50%);
465
+ border: 4px solid transparent;
466
+ border-top-color: #30363d;
467
+ z-index: 1000;
468
+ }
469
+
470
+ .loading, #error {
471
+ text-align: center;
472
+ padding: 40px;
473
+ color: #7d8590;
474
+ }
475
+
476
+ #error {
477
+ color: #f85149;
478
+ }
479
+
480
+ .no-sessions {
481
+ text-align: center;
482
+ padding: 40px;
483
+ color: #7d8590;
484
+ font-style: italic;
485
+ }
486
+
487
+ .session-detail {
488
+ display: none;
489
+ margin-top: 20px;
490
+ }
491
+
492
+ .session-detail.active {
493
+ display: block;
494
+ }
495
+
496
+ .detail-header {
497
+ display: flex;
498
+ justify-content: space-between;
499
+ align-items: center;
500
+ padding: 16px 0;
501
+ border-bottom: 1px solid #30363d;
502
+ margin-bottom: 20px;
503
+ }
504
+
505
+ .detail-title {
506
+ color: #d57455;
507
+ font-size: 1.1rem;
508
+ }
509
+
510
+ .detail-actions {
511
+ display: flex;
512
+ gap: 12px;
513
+ align-items: center;
514
+ }
515
+
516
+ .export-format-select {
517
+ background: #21262d;
518
+ border: 1px solid #30363d;
519
+ color: #c9d1d9;
520
+ padding: 6px 12px;
521
+ border-radius: 4px;
522
+ font-family: inherit;
523
+ font-size: 0.875rem;
524
+ cursor: pointer;
525
+ }
526
+
527
+ .export-format-select:focus {
528
+ outline: none;
529
+ border-color: #d57455;
530
+ }
531
+
532
+ .export-format-select option {
533
+ background: #21262d;
534
+ color: #c9d1d9;
535
+ }
536
+
537
+ .btn {
538
+ background: none;
539
+ border: 1px solid #30363d;
540
+ color: #7d8590;
541
+ padding: 6px 12px;
542
+ border-radius: 4px;
543
+ cursor: pointer;
544
+ font-family: inherit;
545
+ font-size: 0.875rem;
546
+ transition: all 0.2s ease;
547
+ }
548
+
549
+ .btn:hover {
550
+ border-color: #d57455;
551
+ color: #d57455;
552
+ }
553
+
554
+ .btn-primary {
555
+ background: #d57455;
556
+ border-color: #d57455;
557
+ color: #0d1117;
558
+ }
559
+
560
+ .btn-primary:hover {
561
+ background: #e8956f;
562
+ border-color: #e8956f;
563
+ }
564
+
565
+ .session-info {
566
+ display: grid;
567
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
568
+ gap: 20px;
569
+ margin-bottom: 30px;
570
+ }
571
+
572
+ .info-item {
573
+ display: flex;
574
+ flex-direction: column;
575
+ gap: 4px;
576
+ }
577
+
578
+ .info-label {
579
+ color: #7d8590;
580
+ font-size: 0.75rem;
581
+ text-transform: uppercase;
582
+ }
583
+
584
+ .info-value {
585
+ color: #c9d1d9;
586
+ font-size: 0.875rem;
587
+ }
588
+
589
+ .info-value.model {
590
+ color: #a5d6ff;
591
+ font-weight: bold;
592
+ }
593
+
594
+ .search-input {
595
+ width: 100%;
596
+ background: #21262d;
597
+ border: 1px solid #30363d;
598
+ color: #c9d1d9;
599
+ padding: 8px 12px;
600
+ border-radius: 4px;
601
+ font-family: inherit;
602
+ font-size: 0.875rem;
603
+ margin-bottom: 16px;
604
+ }
605
+
606
+ .search-input:focus {
607
+ outline: none;
608
+ border-color: #d57455;
609
+ }
610
+
611
+ .conversation-history {
612
+ border: 1px solid #30363d;
613
+ border-radius: 6px;
614
+ max-height: 600px;
615
+ overflow-y: auto;
616
+ }
617
+
618
+ .message {
619
+ padding: 16px;
620
+ border-bottom: 1px solid #21262d;
621
+ position: relative;
622
+ }
623
+
624
+ .message:last-child {
625
+ border-bottom: none;
626
+ }
627
+
628
+ .message-header {
629
+ display: flex;
630
+ justify-content: space-between;
631
+ align-items: center;
632
+ margin-bottom: 8px;
633
+ }
634
+
635
+ .message-role {
636
+ color: #58a6ff;
637
+ font-size: 0.875rem;
638
+ font-weight: bold;
639
+ }
640
+
641
+ .message-role.user {
642
+ color: #3fb950;
643
+ }
644
+
645
+ .message-role.assistant {
646
+ color: #d57455;
647
+ }
648
+
649
+ .message-time {
650
+ color: #7d8590;
651
+ font-size: 0.75rem;
652
+ }
653
+
654
+ .message-content {
655
+ color: #c9d1d9;
656
+ font-size: 0.875rem;
657
+ line-height: 1.5;
658
+ white-space: pre-wrap;
659
+ word-wrap: break-word;
660
+ }
661
+
662
+ .message-type-indicator {
663
+ position: absolute;
664
+ top: 8px;
665
+ right: 8px;
666
+ width: 8px;
667
+ height: 8px;
668
+ border-radius: 2px;
669
+ cursor: help;
670
+ }
671
+
672
+ .message-type-indicator.success {
673
+ background: #d57455;
674
+ }
675
+
676
+ .message-type-indicator.tool {
677
+ background: #f97316;
678
+ }
679
+
680
+ .message-type-indicator.error {
681
+ background: #dc2626;
682
+ }
683
+
684
+ .message-type-indicator.pending {
685
+ background: #6b7280;
686
+ }
687
+
688
+ .message-type-indicator:hover::after {
689
+ content: attr(data-tooltip);
690
+ position: absolute;
691
+ top: 100%;
692
+ right: 0;
693
+ background: #1c1c1c;
694
+ color: #fff;
695
+ padding: 4px 8px;
696
+ border-radius: 4px;
697
+ font-size: 0.75rem;
698
+ white-space: nowrap;
699
+ z-index: 1000;
700
+ margin-top: 4px;
701
+ border: 1px solid #30363d;
702
+ }
703
+
704
+ .back-btn {
705
+ margin-bottom: 20px;
706
+ }
707
+
708
+ @media (max-width: 768px) {
709
+ .stats-bar {
710
+ gap: 20px;
711
+ }
712
+
713
+ .chart-controls {
714
+ flex-direction: column;
715
+ gap: 12px;
716
+ align-items: stretch;
717
+ }
718
+
719
+ .chart-controls-left {
720
+ flex-direction: column;
721
+ gap: 12px;
722
+ }
723
+
724
+ .chart-controls-right {
725
+ justify-content: center;
726
+ }
727
+
728
+ .charts-container {
729
+ grid-template-columns: 1fr;
730
+ gap: 20px;
731
+ margin: 20px 0;
732
+ }
733
+
734
+ .chart-card {
735
+ padding: 16px;
736
+ }
737
+
738
+ .chart-canvas {
739
+ height: 180px !important;
740
+ }
741
+
742
+ .filter-bar {
743
+ flex-direction: column;
744
+ align-items: flex-start;
745
+ gap: 8px;
746
+ }
747
+
748
+ .sessions-table {
749
+ font-size: 0.8rem;
750
+ }
751
+
752
+ .sessions-table th,
753
+ .sessions-table td {
754
+ padding: 6px 8px;
755
+ }
756
+
757
+ .github-star-btn {
758
+ position: relative;
759
+ margin-top: 12px;
760
+ align-self: flex-start;
761
+ }
762
+
763
+ .terminal-header {
764
+ display: flex;
765
+ flex-direction: column;
766
+ }
767
+ }
768
+ </style>
769
+ </head>
770
+ <body>
771
+ <div class="terminal">
772
+ <div class="terminal-header">
773
+ <div class="terminal-title">
774
+ <span class="status-dot"></span>
775
+ claude-code-analytics
776
+ </div>
777
+ <div class="terminal-subtitle">real-time monitoring dashboard</div>
778
+ <div class="terminal-subtitle" id="lastUpdate"></div>
779
+
780
+ <a href="https://github.com/davila7/claude-code-templates" target="_blank" class="github-star-btn" title="Give us a star on GitHub to support the project!">
781
+ <span class="star-icon">⭐</span>
782
+ <span>Star on GitHub</span>
783
+ </a>
784
+ </div>
785
+
786
+ <div id="loading" class="loading">
787
+ loading claude code data...
788
+ </div>
789
+
790
+ <div id="error" class="error" style="display: none;">
791
+ error: failed to load claude code data
792
+ </div>
793
+
794
+ <div id="dashboard" style="display: none;">
795
+ <div class="stats-bar">
796
+ <div class="stat">
797
+ <span class="stat-label">conversations:</span>
798
+ <span class="stat-value" id="totalConversations">0</span>
799
+ </div>
800
+ <div class="stat">
801
+ <span class="stat-label">claude sessions:</span>
802
+ <span class="stat-value" id="claudeSessions">0</span>
803
+ <span class="stat-sublabel" id="claudeSessionsDetail"></span>
804
+ </div>
805
+ <div class="stat">
806
+ <span class="stat-label">tokens:</span>
807
+ <span class="stat-value" id="totalTokens">0</span>
808
+ </div>
809
+ <div class="stat">
810
+ <span class="stat-label">projects:</span>
811
+ <span class="stat-value" id="activeProjects">0</span>
812
+ </div>
813
+ <div class="stat">
814
+ <span class="stat-label">storage:</span>
815
+ <span class="stat-value" id="dataSize">0</span>
816
+ </div>
817
+ </div>
818
+
819
+ <div class="chart-controls">
820
+ <div class="chart-controls-left">
821
+ <div class="date-control">
822
+ <span class="date-label">from:</span>
823
+ <input type="date" id="dateFrom" class="date-input">
824
+ </div>
825
+ <div class="date-control">
826
+ <span class="date-label">to:</span>
827
+ <input type="date" id="dateTo" class="date-input">
828
+ </div>
829
+ </div>
830
+ <div class="chart-controls-right">
831
+ <button class="refresh-btn" onclick="toggleNotifications()" id="notificationBtn">
832
+ enable notifications
833
+ </button>
834
+ <button class="refresh-btn" onclick="refreshCharts()" id="refreshBtn">
835
+ refresh charts
836
+ </button>
837
+ </div>
838
+ </div>
839
+
840
+ <div class="charts-container">
841
+ <div class="chart-card">
842
+ <div class="chart-title">
843
+ 📊 token usage over time
844
+ </div>
845
+ <canvas id="tokenChart" class="chart-canvas"></canvas>
846
+ </div>
847
+
848
+ <div class="chart-card">
849
+ <div class="chart-title">
850
+ 🎯 project activity distribution
851
+ </div>
852
+ <canvas id="projectChart" class="chart-canvas"></canvas>
853
+ </div>
854
+ </div>
855
+
856
+ <div class="filter-bar">
857
+ <span class="filter-label">filter conversations:</span>
858
+ <div class="filter-buttons">
859
+ <button class="filter-btn active" data-filter="active">active</button>
860
+ <button class="filter-btn" data-filter="recent">recent</button>
861
+ <button class="filter-btn" data-filter="inactive">inactive</button>
862
+ <button class="filter-btn" data-filter="all">all</button>
863
+ </div>
864
+ </div>
865
+
866
+ <table class="sessions-table">
867
+ <thead>
868
+ <tr>
869
+ <th>conversation id</th>
870
+ <th>project</th>
871
+ <th>model</th>
872
+ <th>messages</th>
873
+ <th>tokens</th>
874
+ <th>last activity</th>
875
+ <th>conversation state</th>
876
+ <th>status</th>
877
+ </tr>
878
+ </thead>
879
+ <tbody id="sessionsTable">
880
+ <!-- Sessions will be loaded here -->
881
+ </tbody>
882
+ </table>
883
+
884
+ <div id="noSessions" class="no-sessions" style="display: none;">
885
+ no conversations found for current filter
886
+ </div>
887
+
888
+ <div id="sessionDetail" class="session-detail">
889
+ <button class="btn back-btn" onclick="showSessionsList()">← back to conversations</button>
890
+
891
+ <div class="detail-header">
892
+ <div class="detail-title" id="detailTitle">conversation details</div>
893
+ <div class="detail-actions">
894
+ <select id="exportFormat" class="export-format-select">
895
+ <option value="csv">CSV</option>
896
+ <option value="json">JSON</option>
897
+ </select>
898
+ <button class="btn" onclick="exportSession()">export</button>
899
+ <button class="btn btn-primary" onclick="refreshSessionDetail()">refresh</button>
900
+ </div>
901
+ </div>
902
+
903
+ <div class="session-info" id="sessionInfo">
904
+ <!-- Session info will be loaded here -->
905
+ </div>
906
+
907
+ <div>
908
+ <h3 style="color: #7d8590; margin-bottom: 16px; font-size: 0.875rem; text-transform: uppercase;">conversation history</h3>
909
+ <input type="text" id="conversationSearch" class="search-input" placeholder="Search messages...">
910
+ <div class="conversation-history" id="conversationHistory">
911
+ <!-- Conversation history will be loaded here -->
912
+ </div>
913
+ </div>
914
+ </div>
915
+ </div>
916
+ </div>
917
+
918
+ <script>
919
+ let allConversations = [];
920
+ let currentFilter = 'active';
921
+ let currentSession = null;
922
+ let tokenChart = null;
923
+ let projectChart = null;
924
+ let allData = null;
925
+ let notificationsEnabled = false;
926
+ let previousConversationStates = new Map();
927
+
928
+ async function loadData() {
929
+ try {
930
+ const response = await fetch('/api/data');
931
+ const data = await response.json();
932
+
933
+ console.log('Data loaded:', data.timestamp);
934
+
935
+ document.getElementById('loading').style.display = 'none';
936
+ document.getElementById('dashboard').style.display = 'block';
937
+
938
+ // Update timestamp
939
+ document.getElementById('lastUpdate').textContent = `last update: ${data.lastUpdate}`;
940
+
941
+ updateStats(data.summary);
942
+ allConversations = data.conversations;
943
+ allData = data; // Store data globally for access
944
+ window.allData = data; // Keep for backward compatibility
945
+
946
+ // Initialize date inputs on first load
947
+ if (!document.getElementById('dateFrom').value) {
948
+ initializeDateInputs();
949
+ }
950
+
951
+ updateCharts(data);
952
+ updateSessionsTable();
953
+
954
+ // Check for conversation state changes and send notifications
955
+ checkForNotifications(data.conversations);
956
+
957
+ } catch (error) {
958
+ document.getElementById('loading').style.display = 'none';
959
+ document.getElementById('error').style.display = 'block';
960
+ console.error('Failed to load data:', error);
961
+ }
962
+ }
963
+
964
+ // Function to only update conversation data without refreshing charts
965
+ async function loadConversationData() {
966
+ try {
967
+ const response = await fetch('/api/fast-update');
968
+ const data = await response.json();
969
+
970
+ // Only log state changes, not every refresh
971
+ let hasStateChanges = false;
972
+
973
+ // Log conversation state changes
974
+ if (data.conversations && allConversations) {
975
+ data.conversations.forEach(conv => {
976
+ const prevConv = allConversations.find(c => c.id === conv.id);
977
+ if (prevConv && prevConv.conversationState !== conv.conversationState) {
978
+ console.log('🔄 State change: ' + conv.project + ' from "' + prevConv.conversationState + '" to "' + conv.conversationState + '"');
979
+ hasStateChanges = true;
980
+ }
981
+ });
982
+ }
983
+
984
+ // Only log refresh timestamp if there were actual changes
985
+ if (hasStateChanges) {
986
+ console.log('⚡ Update completed at:', new Date().toLocaleTimeString());
987
+ }
988
+
989
+ // Update timestamp
990
+ document.getElementById('lastUpdate').textContent = `last update: ${data.lastUpdate}`;
991
+
992
+ updateStats(data.summary);
993
+ allConversations = data.conversations;
994
+ allData = data; // Store data globally for access
995
+ window.allData = data; // Keep for backward compatibility
996
+
997
+ // Only update sessions table, not charts
998
+ updateSessionsTable();
999
+
1000
+ // Check for conversation state changes and send notifications
1001
+ checkForNotifications(data.conversations);
1002
+
1003
+ } catch (error) {
1004
+ console.error('Failed to refresh conversation data:', error);
1005
+ }
1006
+ }
1007
+
1008
+ // NEW: Function to update ONLY conversation states (ultra-fast)
1009
+ async function updateConversationStatesOnly() {
1010
+ try {
1011
+ const response = await fetch('/api/conversation-state');
1012
+ const data = await response.json();
1013
+
1014
+ // Update only the conversation state fields in the UI
1015
+ data.activeStates.forEach(stateInfo => {
1016
+ // Update in sessions table
1017
+ const sessionRow = document.querySelector('tr[data-session-id="' + stateInfo.id + '"]');
1018
+ if (sessionRow) {
1019
+ const stateCell = sessionRow.querySelector('.conversation-state');
1020
+ if (stateCell && stateCell.textContent !== stateInfo.state) {
1021
+ console.log('⚡ INSTANT State Update: ' + stateInfo.project + ' → "' + stateInfo.state + '"');
1022
+ stateCell.textContent = stateInfo.state;
1023
+ stateCell.className = 'conversation-state ' + getStateClass(stateInfo.state);
1024
+ }
1025
+ }
1026
+
1027
+ // Update in session detail if visible
1028
+ if (currentSession && currentSession.id === stateInfo.id) {
1029
+ const detailStateElement = document.querySelector('#sessionDetail .conversation-state');
1030
+ if (detailStateElement && detailStateElement.textContent !== stateInfo.state) {
1031
+ detailStateElement.textContent = stateInfo.state;
1032
+ detailStateElement.className = 'conversation-state ' + getStateClass(stateInfo.state);
1033
+ }
1034
+ }
1035
+ });
1036
+
1037
+ } catch (error) {
1038
+ // Silently fail - don't interfere with main data flow
1039
+ }
1040
+ }
1041
+
1042
+ // Notification functions
1043
+ async function requestNotificationPermission() {
1044
+ if (!('Notification' in window)) {
1045
+ console.log('This browser does not support notifications');
1046
+ return false;
1047
+ }
1048
+
1049
+ if (Notification.permission === 'granted') {
1050
+ notificationsEnabled = true;
1051
+ return true;
1052
+ }
1053
+
1054
+ if (Notification.permission !== 'denied') {
1055
+ const permission = await Notification.requestPermission();
1056
+ notificationsEnabled = permission === 'granted';
1057
+ return notificationsEnabled;
1058
+ }
1059
+
1060
+ return false;
1061
+ }
1062
+
1063
+ function sendNotification(title, body, conversationId) {
1064
+ if (!notificationsEnabled) return;
1065
+
1066
+ const notification = new Notification(title, {
1067
+ body: body,
1068
+ icon: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHZpZXdCb3g9IjAgMCAzMiAzMiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPHJlY3Qgd2lkdGg9IjMyIiBoZWlnaHQ9IjMyIiByeD0iNCIgZmlsbD0iIzIxMjYyZCIvPgo8cGF0aCBkPSJNOCA4aDE2djE2SDh6IiBmaWxsPSIjZDU3NDU1Ii8+CjwvZGJnPgo=',
1069
+ badge: 'data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMTYiIGhlaWdodD0iMTYiIHZpZXdCb3g9IjAgMCAxNiAxNiIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj4KPGNpcmNsZSBjeD0iOCIgY3k9IjgiIHI9IjgiIGZpbGw9IiNkNTc0NTUiLz4KPC9zdmc+',
1070
+ tag: conversationId,
1071
+ requireInteraction: true
1072
+ });
1073
+
1074
+ notification.onclick = function() {
1075
+ window.focus();
1076
+ this.close();
1077
+ // Focus on the conversation if possible
1078
+ if (conversationId) {
1079
+ showSessionDetail(conversationId);
1080
+ }
1081
+ };
1082
+
1083
+ // Auto close after 10 seconds
1084
+ setTimeout(() => {
1085
+ notification.close();
1086
+ }, 10000);
1087
+ }
1088
+
1089
+ function checkForNotifications(conversations) {
1090
+ if (!notificationsEnabled) return;
1091
+
1092
+ conversations.forEach(conv => {
1093
+ const currentState = conv.conversationState;
1094
+ const prevState = previousConversationStates.get(conv.id);
1095
+
1096
+ // Check if conversation state changed to "Awaiting user input..."
1097
+ if (prevState && prevState !== currentState) {
1098
+ if (currentState === 'Awaiting user input...' ||
1099
+ currentState === 'User may be typing...' ||
1100
+ currentState === 'Awaiting response...') {
1101
+
1102
+ const title = 'Claude is waiting for you!';
1103
+ const body = `Project: ${conv.project} - Claude needs your input`;
1104
+
1105
+ sendNotification(title, body, conv.id);
1106
+ }
1107
+ }
1108
+
1109
+ // Update previous state
1110
+ previousConversationStates.set(conv.id, currentState);
1111
+ });
1112
+ }
1113
+
1114
+ async function toggleNotifications() {
1115
+ const btn = document.getElementById('notificationBtn');
1116
+
1117
+ if (!notificationsEnabled) {
1118
+ const granted = await requestNotificationPermission();
1119
+ if (granted) {
1120
+ btn.textContent = 'notifications on';
1121
+ btn.style.borderColor = '#3fb950';
1122
+ btn.style.color = '#3fb950';
1123
+
1124
+ // Send a test notification
1125
+ sendNotification(
1126
+ 'Notifications enabled!',
1127
+ 'You will now receive alerts when Claude is waiting for your input.',
1128
+ null
1129
+ );
1130
+ } else {
1131
+ btn.textContent = 'notifications denied';
1132
+ btn.style.borderColor = '#f85149';
1133
+ btn.style.color = '#f85149';
1134
+ }
1135
+ } else {
1136
+ // Disable notifications
1137
+ notificationsEnabled = false;
1138
+ btn.textContent = 'enable notifications';
1139
+ btn.style.borderColor = '#30363d';
1140
+ btn.style.color = '#7d8590';
1141
+ }
1142
+ }
1143
+
1144
+ function updateStats(summary) {
1145
+ document.getElementById('totalConversations').textContent = summary.totalConversations.toLocaleString();
1146
+ document.getElementById('totalTokens').textContent = summary.totalTokens.toLocaleString();
1147
+ document.getElementById('activeProjects').textContent = summary.activeProjects;
1148
+ document.getElementById('dataSize').textContent = summary.totalFileSize;
1149
+
1150
+ // Update Claude sessions
1151
+ if (summary.claudeSessions) {
1152
+ document.getElementById('claudeSessions').textContent = summary.claudeSessions.total.toLocaleString();
1153
+ document.getElementById('claudeSessionsDetail').textContent =
1154
+ `this month: ${summary.claudeSessions.currentMonth} • this week: ${summary.claudeSessions.thisWeek}`;
1155
+ }
1156
+ }
1157
+
1158
+ function initializeDateInputs() {
1159
+ const today = new Date();
1160
+ const sevenDaysAgo = new Date(today);
1161
+ sevenDaysAgo.setDate(sevenDaysAgo.getDate() - 7);
1162
+
1163
+ document.getElementById('dateFrom').value = sevenDaysAgo.toISOString().split('T')[0];
1164
+ document.getElementById('dateTo').value = today.toISOString().split('T')[0];
1165
+ }
1166
+
1167
+ function getDateRange() {
1168
+ const fromDate = new Date(document.getElementById('dateFrom').value);
1169
+ const toDate = new Date(document.getElementById('dateTo').value);
1170
+ toDate.setHours(23, 59, 59, 999); // Include the entire end date
1171
+
1172
+ return { fromDate, toDate };
1173
+ }
1174
+
1175
+ function filterConversationsByDate(conversations) {
1176
+ const { fromDate, toDate } = getDateRange();
1177
+
1178
+ return conversations.filter(conv => {
1179
+ const convDate = new Date(conv.lastModified);
1180
+ return convDate >= fromDate && convDate <= toDate;
1181
+ });
1182
+ }
1183
+
1184
+ function updateCharts(data) {
1185
+ // Wait for Chart.js to load before creating charts
1186
+ if (typeof Chart === 'undefined') {
1187
+ console.log('Chart.js not loaded yet, retrying in 100ms...');
1188
+ setTimeout(() => updateCharts(data), 100);
1189
+ return;
1190
+ }
1191
+
1192
+ // Use ALL conversations but filter chart display by date range
1193
+ // This maintains the original behavior
1194
+
1195
+ // Update Token Usage Over Time Chart
1196
+ updateTokenChart(data.conversations);
1197
+
1198
+ // Update Project Activity Distribution Chart
1199
+ updateProjectChart(data.conversations);
1200
+ }
1201
+
1202
+ async function refreshCharts() {
1203
+ const refreshBtn = document.getElementById('refreshBtn');
1204
+ refreshBtn.classList.add('loading');
1205
+ refreshBtn.textContent = '🔄 refreshing...';
1206
+
1207
+ try {
1208
+ // Use existing data but re-filter and update charts
1209
+ if (allData) {
1210
+ updateCharts(allData);
1211
+ }
1212
+ } catch (error) {
1213
+ console.error('Error refreshing charts:', error);
1214
+ } finally {
1215
+ refreshBtn.classList.remove('loading');
1216
+ refreshBtn.textContent = 'refresh charts';
1217
+ }
1218
+ }
1219
+
1220
+ function updateTokenChart(conversations) {
1221
+ // Check if Chart.js is available
1222
+ if (typeof Chart === 'undefined') {
1223
+ console.warn('Chart.js not available for updateTokenChart');
1224
+ return;
1225
+ }
1226
+
1227
+ // Prepare data for selected date range
1228
+ const { fromDate, toDate } = getDateRange();
1229
+ const dateRange = [];
1230
+
1231
+ const currentDate = new Date(fromDate);
1232
+ while (currentDate <= toDate) {
1233
+ dateRange.push({
1234
+ date: currentDate.toISOString().split('T')[0],
1235
+ label: currentDate.toLocaleDateString('en-US', { month: 'short', day: 'numeric' }),
1236
+ tokens: 0
1237
+ });
1238
+ currentDate.setDate(currentDate.getDate() + 1);
1239
+ }
1240
+
1241
+ // Aggregate tokens by day
1242
+ conversations.forEach(conv => {
1243
+ const convDate = new Date(conv.lastModified).toISOString().split('T')[0];
1244
+ const dayData = dateRange.find(day => day.date === convDate);
1245
+ if (dayData) {
1246
+ dayData.tokens += conv.tokens;
1247
+ }
1248
+ });
1249
+
1250
+ const ctx = document.getElementById('tokenChart').getContext('2d');
1251
+
1252
+ if (tokenChart) {
1253
+ tokenChart.destroy();
1254
+ }
1255
+
1256
+ tokenChart = new Chart(ctx, {
1257
+ type: 'line',
1258
+ data: {
1259
+ labels: dateRange.map(day => day.label),
1260
+ datasets: [{
1261
+ label: 'Tokens',
1262
+ data: dateRange.map(day => day.tokens),
1263
+ borderColor: '#d57455',
1264
+ backgroundColor: 'rgba(213, 116, 85, 0.1)',
1265
+ borderWidth: 2,
1266
+ pointBackgroundColor: '#d57455',
1267
+ pointBorderColor: '#d57455',
1268
+ pointRadius: 4,
1269
+ pointHoverRadius: 6,
1270
+ fill: true,
1271
+ tension: 0.3
1272
+ }]
1273
+ },
1274
+ options: {
1275
+ responsive: true,
1276
+ maintainAspectRatio: false,
1277
+ plugins: {
1278
+ legend: {
1279
+ display: false
1280
+ },
1281
+ tooltip: {
1282
+ backgroundColor: '#161b22',
1283
+ titleColor: '#d57455',
1284
+ bodyColor: '#c9d1d9',
1285
+ borderColor: '#30363d',
1286
+ borderWidth: 1,
1287
+ titleFont: {
1288
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1289
+ size: 12
1290
+ },
1291
+ bodyFont: {
1292
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1293
+ size: 11
1294
+ },
1295
+ callbacks: {
1296
+ title: function(context) {
1297
+ return context[0].label;
1298
+ },
1299
+ label: function(context) {
1300
+ return `Tokens: ${context.parsed.y.toLocaleString()}`;
1301
+ }
1302
+ }
1303
+ }
1304
+ },
1305
+ interaction: {
1306
+ intersect: false,
1307
+ mode: 'index'
1308
+ },
1309
+ hover: {
1310
+ animationDuration: 200
1311
+ },
1312
+ scales: {
1313
+ x: {
1314
+ grid: {
1315
+ color: '#30363d',
1316
+ borderColor: '#30363d'
1317
+ },
1318
+ ticks: {
1319
+ color: '#7d8590',
1320
+ font: {
1321
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1322
+ size: 11
1323
+ }
1324
+ }
1325
+ },
1326
+ y: {
1327
+ grid: {
1328
+ color: '#30363d',
1329
+ borderColor: '#30363d'
1330
+ },
1331
+ ticks: {
1332
+ color: '#7d8590',
1333
+ font: {
1334
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1335
+ size: 11
1336
+ },
1337
+ callback: function(value) {
1338
+ return value.toLocaleString();
1339
+ }
1340
+ }
1341
+ }
1342
+ }
1343
+ }
1344
+ });
1345
+ }
1346
+
1347
+ function updateProjectChart(conversations) {
1348
+ // Check if Chart.js is available
1349
+ if (typeof Chart === 'undefined') {
1350
+ console.warn('Chart.js not available for updateProjectChart');
1351
+ return;
1352
+ }
1353
+
1354
+ // Aggregate data by project
1355
+ const projectData = {};
1356
+
1357
+ conversations.forEach(conv => {
1358
+ if (!projectData[conv.project]) {
1359
+ projectData[conv.project] = 0;
1360
+ }
1361
+ projectData[conv.project] += conv.tokens;
1362
+ });
1363
+
1364
+ // Get top 5 projects and group others
1365
+ const sortedProjects = Object.entries(projectData)
1366
+ .sort(([,a], [,b]) => b - a)
1367
+ .slice(0, 5);
1368
+
1369
+ const othersTotal = Object.entries(projectData)
1370
+ .slice(5)
1371
+ .reduce((sum, [,tokens]) => sum + tokens, 0);
1372
+
1373
+ if (othersTotal > 0) {
1374
+ sortedProjects.push(['others', othersTotal]);
1375
+ }
1376
+
1377
+ // Terminal-style colors
1378
+ const colors = [
1379
+ '#d57455', // Orange
1380
+ '#3fb950', // Green
1381
+ '#a5d6ff', // Blue
1382
+ '#f97316', // Orange variant
1383
+ '#c9d1d9', // Light gray
1384
+ '#7d8590' // Gray
1385
+ ];
1386
+
1387
+ const ctx = document.getElementById('projectChart').getContext('2d');
1388
+
1389
+ if (projectChart) {
1390
+ projectChart.destroy();
1391
+ }
1392
+
1393
+ projectChart = new Chart(ctx, {
1394
+ type: 'doughnut',
1395
+ data: {
1396
+ labels: sortedProjects.map(([project]) => project),
1397
+ datasets: [{
1398
+ data: sortedProjects.map(([,tokens]) => tokens),
1399
+ backgroundColor: colors.slice(0, sortedProjects.length),
1400
+ borderColor: '#161b22',
1401
+ borderWidth: 2,
1402
+ hoverBorderWidth: 3
1403
+ }]
1404
+ },
1405
+ options: {
1406
+ responsive: true,
1407
+ maintainAspectRatio: false,
1408
+ plugins: {
1409
+ legend: {
1410
+ position: 'bottom',
1411
+ labels: {
1412
+ color: '#7d8590',
1413
+ font: {
1414
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace',
1415
+ size: 10
1416
+ },
1417
+ padding: 15,
1418
+ usePointStyle: true,
1419
+ pointStyle: 'circle'
1420
+ }
1421
+ },
1422
+ tooltip: {
1423
+ backgroundColor: '#161b22',
1424
+ titleColor: '#d57455',
1425
+ bodyColor: '#c9d1d9',
1426
+ borderColor: '#30363d',
1427
+ borderWidth: 1,
1428
+ titleFont: {
1429
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace'
1430
+ },
1431
+ bodyFont: {
1432
+ family: 'Monaco, Menlo, Ubuntu Mono, monospace'
1433
+ },
1434
+ callbacks: {
1435
+ label: function(context) {
1436
+ const total = context.dataset.data.reduce((sum, value) => sum + value, 0);
1437
+ const percentage = ((context.parsed / total) * 100).toFixed(1);
1438
+ return `${context.label}: ${context.parsed.toLocaleString()} tokens (${percentage}%)`;
1439
+ }
1440
+ }
1441
+ }
1442
+ },
1443
+ cutout: '60%'
1444
+ }
1445
+ });
1446
+ }
1447
+
1448
+ function updateSessionsTable() {
1449
+ const tableBody = document.getElementById('sessionsTable');
1450
+ const noSessionsDiv = document.getElementById('noSessions');
1451
+
1452
+ // Filter conversations based on current filter
1453
+ let filteredConversations = allConversations;
1454
+ if (currentFilter !== 'all') {
1455
+ filteredConversations = allConversations.filter(conv => conv.status === currentFilter);
1456
+ }
1457
+
1458
+ if (filteredConversations.length === 0) {
1459
+ tableBody.innerHTML = '';
1460
+ noSessionsDiv.style.display = 'block';
1461
+ return;
1462
+ }
1463
+
1464
+ noSessionsDiv.style.display = 'none';
1465
+
1466
+ tableBody.innerHTML = filteredConversations.map(conv => `
1467
+ <tr onclick="showSessionDetail('${conv.id}')" style="cursor: pointer;">
1468
+ <td>
1469
+ <div class="session-id-container">
1470
+ <div class="session-id">
1471
+ ${conv.id.substring(0, 8)}...
1472
+ ${conv.runningProcess ? `<span class="process-indicator" title="Active claude process (PID: ${conv.runningProcess.pid})"></span>` : ''}
1473
+ </div>
1474
+ <div class="status-squares">
1475
+ ${generateStatusSquaresHTML(conv.statusSquares || [])}
1476
+ </div>
1477
+ </div>
1478
+ </td>
1479
+ <td class="session-project">${conv.project}</td>
1480
+ <td class="session-model" title="${conv.modelInfo ? conv.modelInfo.primaryModel + ' (' + conv.modelInfo.currentServiceTier + ')' : 'N/A'}">${conv.modelInfo ? conv.modelInfo.primaryModel : 'N/A'}</td>
1481
+ <td class="session-messages">${conv.messageCount}</td>
1482
+ <td class="session-tokens">${conv.tokens.toLocaleString()}</td>
1483
+ <td class="session-time">${formatTime(conv.lastModified)}</td>
1484
+ <td class="conversation-state ${getStateClass(conv.conversationState)}">${conv.conversationState}</td>
1485
+ <td class="status-${conv.status}">${conv.status}</td>
1486
+ </tr>
1487
+ `).join('');
1488
+
1489
+ // NEW: Add orphan processes (active claude commands without conversation)
1490
+ if (window.allData && window.allData.orphanProcesses && window.allData.orphanProcesses.length > 0) {
1491
+ const orphanRows = window.allData.orphanProcesses.map(process => `
1492
+ <tr style="background: rgba(248, 81, 73, 0.1); cursor: default;">
1493
+ <td>
1494
+ <div class="session-id-container">
1495
+ <div class="session-id">
1496
+ orphan-${process.pid}
1497
+ <span class="process-indicator orphan" title="Orphan claude process (PID: ${process.pid})"></span>
1498
+ </div>
1499
+ </div>
1500
+ </td>
1501
+ <td class="session-project">${process.workingDir}</td>
1502
+ <td class="session-model">Unknown</td>
1503
+ <td class="session-messages">-</td>
1504
+ <td class="session-tokens">-</td>
1505
+ <td class="session-time">Running</td>
1506
+ <td class="conversation-state">Active process</td>
1507
+ <td class="status-active">orphan</td>
1508
+ </tr>
1509
+ `).join('');
1510
+
1511
+ if (currentFilter === 'active' || currentFilter === 'all') {
1512
+ tableBody.innerHTML += orphanRows;
1513
+ }
1514
+ }
1515
+ }
1516
+
1517
+ function formatTime(date) {
1518
+ const now = new Date();
1519
+ const diff = now - new Date(date);
1520
+ const minutes = Math.floor(diff / (1000 * 60));
1521
+ const hours = Math.floor(minutes / 60);
1522
+ const days = Math.floor(hours / 24);
1523
+
1524
+ if (minutes < 1) return 'now';
1525
+ if (minutes < 60) return `${minutes}m ago`;
1526
+ if (hours < 24) return `${hours}h ago`;
1527
+ return `${days}d ago`;
1528
+ }
1529
+
1530
+ function formatMessageTime(timestamp) {
1531
+ const date = new Date(timestamp);
1532
+ return date.toLocaleTimeString('en-US', {
1533
+ hour12: false,
1534
+ hour: '2-digit',
1535
+ minute: '2-digit',
1536
+ second: '2-digit'
1537
+ });
1538
+ }
1539
+
1540
+ function getStateClass(conversationState) {
1541
+ if (conversationState.includes('working') || conversationState.includes('Working')) {
1542
+ return 'working';
1543
+ }
1544
+ if (conversationState.includes('typing') || conversationState.includes('Typing')) {
1545
+ return 'typing';
1546
+ }
1547
+ return '';
1548
+ }
1549
+
1550
+ function generateStatusSquaresHTML(statusSquares) {
1551
+ if (!statusSquares || statusSquares.length === 0) {
1552
+ return '';
1553
+ }
1554
+
1555
+ return statusSquares.map(square =>
1556
+ `<div class="status-square ${square.type}" data-tooltip="${square.tooltip}"></div>`
1557
+ ).join('');
1558
+ }
1559
+
1560
+ function getMessageType(message) {
1561
+ if (message.role === 'user') {
1562
+ return {
1563
+ type: 'pending',
1564
+ tooltip: 'User input'
1565
+ };
1566
+ } else if (message.role === 'assistant') {
1567
+ const content = message.content || '';
1568
+
1569
+ if (typeof content === 'string') {
1570
+ if (content.includes('[Tool:') || content.includes('tool_use')) {
1571
+ return {
1572
+ type: 'tool',
1573
+ tooltip: 'Tool execution'
1574
+ };
1575
+ } else if (content.includes('error') || content.includes('Error') || content.includes('failed')) {
1576
+ return {
1577
+ type: 'error',
1578
+ tooltip: 'Error in response'
1579
+ };
1580
+ } else {
1581
+ return {
1582
+ type: 'success',
1583
+ tooltip: 'Successful response'
1584
+ };
1585
+ }
1586
+ }
1587
+ }
1588
+
1589
+ return {
1590
+ type: 'success',
1591
+ tooltip: 'Message'
1592
+ };
1593
+ }
1594
+
1595
+ // Filter button handlers
1596
+ document.addEventListener('DOMContentLoaded', function() {
1597
+ const filterButtons = document.querySelectorAll('.filter-btn');
1598
+
1599
+ filterButtons.forEach(button => {
1600
+ button.addEventListener('click', function() {
1601
+ // Remove active class from all buttons
1602
+ filterButtons.forEach(btn => btn.classList.remove('active'));
1603
+
1604
+ // Add active class to clicked button
1605
+ this.classList.add('active');
1606
+
1607
+ // Update current filter
1608
+ currentFilter = this.dataset.filter;
1609
+
1610
+ // Update table
1611
+ updateSessionsTable();
1612
+ });
1613
+ });
1614
+ });
1615
+
1616
+ // Session detail functions
1617
+ async function showSessionDetail(sessionId) {
1618
+ currentSession = allConversations.find(conv => conv.id === sessionId);
1619
+ if (!currentSession) return;
1620
+
1621
+ // Hide sessions list and show detail
1622
+ document.querySelector('.filter-bar').style.display = 'none';
1623
+ document.querySelector('.sessions-table').style.display = 'none';
1624
+ document.getElementById('noSessions').style.display = 'none';
1625
+ document.getElementById('sessionDetail').classList.add('active');
1626
+
1627
+ // Update title
1628
+ document.getElementById('detailTitle').textContent = `conversation: ${sessionId.substring(0, 8)}...`;
1629
+
1630
+ // Load session info
1631
+ updateSessionInfo(currentSession);
1632
+
1633
+ // Load conversation history
1634
+ await loadConversationHistory(currentSession);
1635
+ }
1636
+
1637
+ function showSessionsList() {
1638
+ document.getElementById('sessionDetail').classList.remove('active');
1639
+ document.querySelector('.filter-bar').style.display = 'flex';
1640
+ document.querySelector('.sessions-table').style.display = 'table';
1641
+ updateSessionsTable();
1642
+ currentSession = null;
1643
+ }
1644
+
1645
+ function updateSessionInfo(session) {
1646
+ const container = document.getElementById('sessionInfo');
1647
+
1648
+ container.innerHTML = `
1649
+ <div class="info-item">
1650
+ <div class="info-label">conversation id</div>
1651
+ <div class="info-value">${session.id}</div>
1652
+ </div>
1653
+ <div class="info-item">
1654
+ <div class="info-label">project</div>
1655
+ <div class="info-value">${session.project}</div>
1656
+ </div>
1657
+ <div class="info-item">
1658
+ <div class="info-label">messages</div>
1659
+ <div class="info-value">${session.messageCount}</div>
1660
+ </div>
1661
+ <div class="info-item">
1662
+ <div class="info-label">total tokens</div>
1663
+ <div class="info-value">${session.tokens.toLocaleString()}</div>
1664
+ </div>
1665
+ <div class="info-item">
1666
+ <div class="info-label">model</div>
1667
+ <div class="info-value model">${session.modelInfo ? session.modelInfo.primaryModel : 'N/A'}</div>
1668
+ </div>
1669
+ <div class="info-item">
1670
+ <div class="info-label">service tier</div>
1671
+ <div class="info-value">${session.modelInfo ? session.modelInfo.currentServiceTier : 'N/A'}</div>
1672
+ </div>
1673
+ <div class="info-item">
1674
+ <div class="info-label">token details</div>
1675
+ <div class="info-value">${session.tokenUsage ? session.tokenUsage.inputTokens.toLocaleString() + ' in / ' + session.tokenUsage.outputTokens.toLocaleString() + ' out' : 'N/A'}</div>
1676
+ </div>
1677
+ ${session.tokenUsage && (session.tokenUsage.cacheCreationTokens > 0 || session.tokenUsage.cacheReadTokens > 0) ?
1678
+ `<div class="info-item">
1679
+ <div class="info-label">cache tokens</div>
1680
+ <div class="info-value">${session.tokenUsage.cacheCreationTokens.toLocaleString()} created / ${session.tokenUsage.cacheReadTokens.toLocaleString()} read</div>
1681
+ </div>` : ''
1682
+ }
1683
+ <div class="info-item">
1684
+ <div class="info-label">file size</div>
1685
+ <div class="info-value">${formatBytes(session.fileSize)}</div>
1686
+ </div>
1687
+ <div class="info-item">
1688
+ <div class="info-label">created</div>
1689
+ <div class="info-value">${new Date(session.created).toLocaleString()}</div>
1690
+ </div>
1691
+ <div class="info-item">
1692
+ <div class="info-label">last modified</div>
1693
+ <div class="info-value">${new Date(session.lastModified).toLocaleString()}</div>
1694
+ </div>
1695
+ <div class="info-item">
1696
+ <div class="info-label">conversation state</div>
1697
+ <div class="info-value conversation-state ${getStateClass(session.conversationState)}">${session.conversationState}</div>
1698
+ </div>
1699
+ <div class="info-item">
1700
+ <div class="info-label">status</div>
1701
+ <div class="info-value status-${session.status}">${session.status}</div>
1702
+ </div>
1703
+ `;
1704
+ }
1705
+
1706
+ async function loadConversationHistory(session) {
1707
+ try {
1708
+ const response = await fetch(`/api/session/${session.id}`);
1709
+ const sessionData = await response.json();
1710
+
1711
+ const container = document.getElementById('conversationHistory');
1712
+ const searchInput = document.getElementById('conversationSearch');
1713
+
1714
+ if (!sessionData.messages || sessionData.messages.length === 0) {
1715
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: #7d8590;">no messages found</div>';
1716
+ searchInput.style.display = 'none';
1717
+ return;
1718
+ }
1719
+ searchInput.style.display = 'block';
1720
+
1721
+ const renderMessages = (filter = '') => {
1722
+ const lowerCaseFilter = filter.toLowerCase();
1723
+ const filteredMessages = sessionData.messages.filter(m =>
1724
+ (m.content || '').toLowerCase().includes(lowerCaseFilter)
1725
+ );
1726
+
1727
+ if (filteredMessages.length === 0) {
1728
+ container.innerHTML = '<div style="padding: 20px; text-align: center; color: #7d8590;">No messages match your search.</div>';
1729
+ return;
1730
+ }
1731
+
1732
+ const reversedMessages = filteredMessages.slice().reverse();
1733
+
1734
+ container.innerHTML = reversedMessages.map((message, index) => {
1735
+ const messageType = getMessageType(message);
1736
+ const messageNum = filteredMessages.length - index;
1737
+ return `
1738
+ <div class="message">
1739
+ <div class="message-type-indicator ${messageType.type}" data-tooltip="${messageType.tooltip}"></div>
1740
+ <div class="message-header">
1741
+ <div class="message-role ${message.role}">${message.role}</div>
1742
+ <div class="message-time">
1743
+ #${messageNum} • ${message.timestamp ? formatMessageTime(message.timestamp) : 'unknown time'}
1744
+ </div>
1745
+ </div>
1746
+ <div class="message-content">${truncateContent(message.content || 'no content')}</div>
1747
+ </div>
1748
+ `;
1749
+ }).join('');
1750
+ }
1751
+
1752
+ renderMessages();
1753
+
1754
+ searchInput.addEventListener('input', () => {
1755
+ renderMessages(searchInput.value);
1756
+ });
1757
+
1758
+ } catch (error) {
1759
+ document.getElementById('conversationHistory').innerHTML =
1760
+ '<div style="padding: 20px; text-align: center; color: #f85149;">error loading conversation history</div>';
1761
+ console.error('Failed to load conversation history:', error);
1762
+ }
1763
+ }
1764
+
1765
+ function truncateContent(content, maxLength = 1000) {
1766
+ if (typeof content !== 'string') return 'no content';
1767
+ if (!content.trim()) return 'empty message';
1768
+ if (content.length <= maxLength) return content;
1769
+ return content.substring(0, maxLength) + '\n\n[... message truncated ...]';
1770
+ }
1771
+
1772
+ function formatBytes(bytes) {
1773
+ if (bytes === 0) return '0 B';
1774
+ const k = 1024;
1775
+ const sizes = ['B', 'KB', 'MB', 'GB'];
1776
+ const i = Math.floor(Math.log(bytes) / Math.log(k));
1777
+ return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
1778
+ }
1779
+
1780
+ function exportSession() {
1781
+ if (!currentSession) return;
1782
+
1783
+ const format = document.getElementById('exportFormat').value;
1784
+
1785
+ // Fetch conversation history and export
1786
+ fetch(`/api/session/${currentSession.id}`)
1787
+ .then(response => response.json())
1788
+ .then(sessionData => {
1789
+ if (format === 'csv') {
1790
+ exportSessionAsCSV(sessionData);
1791
+ } else if (format === 'json') {
1792
+ exportSessionAsJSON(sessionData);
1793
+ }
1794
+ })
1795
+ .catch(error => {
1796
+ console.error(`Failed to export ${format.toUpperCase()}:`, error);
1797
+ alert(`Failed to export ${format.toUpperCase()}. Please try again.`);
1798
+ });
1799
+ }
1800
+
1801
+ function exportSessionAsCSV(sessionData) {
1802
+ // Create CSV content
1803
+ let csvContent = 'Conversation ID,Project,Message Count,Tokens,File Size,Created,Last Modified,Conversation State,Status\n';
1804
+ csvContent += `"${currentSession.id}","${currentSession.project}",${currentSession.messageCount},${currentSession.tokens},${currentSession.fileSize},"${new Date(currentSession.created).toISOString()}","${new Date(currentSession.lastModified).toISOString()}","${currentSession.conversationState}","${currentSession.status}"\n\n`;
1805
+
1806
+ csvContent += 'Message #,Role,Timestamp,Content\n';
1807
+
1808
+ // Add conversation history
1809
+ if (sessionData.messages) {
1810
+ sessionData.messages.forEach((message, index) => {
1811
+ const content = (message.content || 'no content').replace(/"/g, '""');
1812
+ const timestamp = message.timestamp ? new Date(message.timestamp).toISOString() : 'unknown';
1813
+ csvContent += `${index + 1},"${message.role}","${timestamp}","${content}"\n`;
1814
+ });
1815
+ }
1816
+
1817
+ // Download CSV
1818
+ const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' });
1819
+ downloadFile(blob, `claude-conversation-${currentSession.id.substring(0, 8)}.csv`);
1820
+ }
1821
+
1822
+ function exportSessionAsJSON(sessionData) {
1823
+ // Create comprehensive JSON export
1824
+ const exportData = {
1825
+ conversation: {
1826
+ id: currentSession.id,
1827
+ filename: currentSession.filename,
1828
+ project: currentSession.project,
1829
+ messageCount: currentSession.messageCount,
1830
+ tokens: currentSession.tokens,
1831
+ fileSize: currentSession.fileSize,
1832
+ created: currentSession.created,
1833
+ lastModified: currentSession.lastModified,
1834
+ conversationState: currentSession.conversationState,
1835
+ status: currentSession.status
1836
+ },
1837
+ messages: sessionData.messages || [],
1838
+ metadata: {
1839
+ exportedAt: new Date().toISOString(),
1840
+ exportFormat: 'json',
1841
+ toolVersion: '1.5.7'
1842
+ }
1843
+ };
1844
+
1845
+ // Download JSON
1846
+ const jsonString = JSON.stringify(exportData, null, 2);
1847
+ const blob = new Blob([jsonString], { type: 'application/json;charset=utf-8;' });
1848
+ downloadFile(blob, `claude-conversation-${currentSession.id.substring(0, 8)}.json`);
1849
+ }
1850
+
1851
+ function downloadFile(blob, filename) {
1852
+ const link = document.createElement('a');
1853
+ const url = URL.createObjectURL(blob);
1854
+ link.setAttribute('href', url);
1855
+ link.setAttribute('download', filename);
1856
+ link.style.visibility = 'hidden';
1857
+ document.body.appendChild(link);
1858
+ link.click();
1859
+ document.body.removeChild(link);
1860
+ URL.revokeObjectURL(url);
1861
+ }
1862
+
1863
+ function refreshSessionDetail() {
1864
+ if (currentSession) {
1865
+ loadConversationHistory(currentSession);
1866
+ }
1867
+ }
1868
+
1869
+ // Manual refresh function
1870
+ async function forceRefresh() {
1871
+ try {
1872
+ const response = await fetch('/api/refresh');
1873
+ const result = await response.json();
1874
+ console.log('Manual refresh:', result);
1875
+ await loadData();
1876
+ } catch (error) {
1877
+ console.error('Failed to refresh:', error);
1878
+ }
1879
+ }
1880
+
1881
+ // Wait for DOM and Chart.js to load
1882
+ document.addEventListener('DOMContentLoaded', function() {
1883
+ // Check if Chart.js is loaded
1884
+ function initWhenReady() {
1885
+ if (typeof Chart !== 'undefined') {
1886
+ console.log('Chart.js loaded successfully');
1887
+ loadData();
1888
+
1889
+ // Regular refresh for conversation data every 1 second (slower)
1890
+ setInterval(() => {
1891
+ loadConversationData();
1892
+ }, 1000);
1893
+
1894
+ // NEW: Ultra-fast refresh ONLY for conversation states (every 100ms)
1895
+ setInterval(() => {
1896
+ updateConversationStatesOnly();
1897
+ }, 100);
1898
+ } else {
1899
+ console.log('Waiting for Chart.js to load...');
1900
+ setTimeout(initWhenReady, 100);
1901
+ }
1902
+ }
1903
+
1904
+ initWhenReady();
1905
+
1906
+ // Add event listeners for date inputs
1907
+ document.getElementById('dateFrom').addEventListener('change', refreshCharts);
1908
+ document.getElementById('dateTo').addEventListener('change', refreshCharts);
1909
+
1910
+ // Initialize notification button state
1911
+ updateNotificationButtonState();
1912
+ });
1913
+
1914
+ function updateNotificationButtonState() {
1915
+ const btn = document.getElementById('notificationBtn');
1916
+ if (!btn) return;
1917
+
1918
+ if (Notification.permission === 'granted') {
1919
+ notificationsEnabled = true;
1920
+ btn.textContent = 'notifications on';
1921
+ btn.style.borderColor = '#3fb950';
1922
+ btn.style.color = '#3fb950';
1923
+ } else if (Notification.permission === 'denied') {
1924
+ btn.textContent = 'notifications denied';
1925
+ btn.style.borderColor = '#f85149';
1926
+ btn.style.color = '#f85149';
1927
+ }
1928
+ }
1929
+
1930
+ // Add keyboard shortcut for refresh (F5 or Ctrl+R)
1931
+ document.addEventListener('keydown', function(e) {
1932
+ if (e.key === 'F5' || (e.ctrlKey && e.key === 'r')) {
1933
+ e.preventDefault();
1934
+ forceRefresh();
1935
+ }
1936
+ });
1937
+ </script>
1938
+ </body>
1939
+ </html>