@makimoto/cc-log-viewer 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1490 @@
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>CC Log Viewer</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:ital,wght@0,300;0,400;0,500;0,600;1,400&family=DM+Sans:ital,wght@0,400;0,500;0,600;1,400&display=swap" rel="stylesheet">
10
+ <style>
11
+ :root {
12
+ --bg-void: #0a0a0c;
13
+ --bg-primary: #0f1014;
14
+ --bg-card: #151519;
15
+ --bg-card-hover: #1a1a20;
16
+ --bg-elevated: #1e1e25;
17
+ --bg-input: #111115;
18
+ --text-primary: #d4d0c8;
19
+ --text-secondary: #908e86;
20
+ --text-muted: #5a584f;
21
+ --text-dim: #3a3830;
22
+ --accent: #d4a24c;
23
+ --accent-glow: #d4a24c40;
24
+ --accent-bright: #e8b85c;
25
+ --accent-dim: #8a6a2e;
26
+ --border: #222228;
27
+ --border-subtle: #1a1a1f;
28
+ --border-accent: #d4a24c30;
29
+ --badge-user: #6ab07c;
30
+ --badge-user-bg: #6ab07c18;
31
+ --badge-assistant: #9b8ec4;
32
+ --badge-assistant-bg: #9b8ec418;
33
+ --badge-project: #5a9bcf;
34
+ --badge-project-bg: #5a9bcf15;
35
+ --badge-branch: #cf8f5a;
36
+ --badge-branch-bg: #cf8f5a15;
37
+ --highlight-bg: #d4a24c25;
38
+ --highlight-text: #e8b85c;
39
+ --danger: #c45a5a;
40
+ --success: #6ab07c;
41
+ --radius-sm: 3px;
42
+ --radius: 6px;
43
+ --radius-lg: 10px;
44
+ --font-mono: "IBM Plex Mono", "SF Mono", monospace;
45
+ --font-body: "DM Sans", -apple-system, sans-serif;
46
+ --shadow-card: 0 1px 3px rgba(0,0,0,0.4), 0 0 0 1px var(--border);
47
+ --shadow-hover: 0 4px 16px rgba(0,0,0,0.5), 0 0 0 1px var(--border-accent);
48
+ --transition: 0.2s cubic-bezier(0.25, 0.46, 0.45, 0.94);
49
+ }
50
+
51
+ *, *::before, *::after {
52
+ box-sizing: border-box;
53
+ margin: 0;
54
+ padding: 0;
55
+ }
56
+
57
+ html {
58
+ scroll-behavior: smooth;
59
+ }
60
+
61
+ body {
62
+ background: var(--bg-void);
63
+ color: var(--text-primary);
64
+ font-family: var(--font-body);
65
+ line-height: 1.6;
66
+ min-height: 100vh;
67
+ -webkit-font-smoothing: antialiased;
68
+ -moz-osx-font-smoothing: grayscale;
69
+ }
70
+
71
+ /* Noise texture overlay */
72
+ body::before {
73
+ content: "";
74
+ position: fixed;
75
+ inset: 0;
76
+ background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 256 256' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noise'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='4' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noise)' opacity='0.03'/%3E%3C/svg%3E");
77
+ pointer-events: none;
78
+ z-index: 9999;
79
+ opacity: 0.5;
80
+ }
81
+
82
+ /* Ambient top glow */
83
+ body::after {
84
+ content: "";
85
+ position: fixed;
86
+ top: -200px;
87
+ left: 50%;
88
+ transform: translateX(-50%);
89
+ width: 800px;
90
+ height: 400px;
91
+ background: radial-gradient(ellipse, var(--accent-glow) 0%, transparent 70%);
92
+ pointer-events: none;
93
+ z-index: 0;
94
+ opacity: 0.4;
95
+ }
96
+
97
+ a {
98
+ color: var(--accent);
99
+ text-decoration: none;
100
+ transition: color var(--transition);
101
+ }
102
+
103
+ a:hover {
104
+ color: var(--accent-bright);
105
+ }
106
+
107
+ ::selection {
108
+ background: var(--accent);
109
+ color: var(--bg-void);
110
+ }
111
+
112
+ /* Scrollbar */
113
+ ::-webkit-scrollbar {
114
+ width: 6px;
115
+ }
116
+ ::-webkit-scrollbar-track {
117
+ background: transparent;
118
+ }
119
+ ::-webkit-scrollbar-thumb {
120
+ background: var(--border);
121
+ border-radius: 3px;
122
+ }
123
+ ::-webkit-scrollbar-thumb:hover {
124
+ background: var(--text-dim);
125
+ }
126
+
127
+ .container {
128
+ max-width: 860px;
129
+ margin: 0 auto;
130
+ padding: 0 24px;
131
+ position: relative;
132
+ z-index: 1;
133
+ }
134
+
135
+ /* ── Header ── */
136
+ header {
137
+ padding: 48px 0 24px;
138
+ text-align: left;
139
+ animation: fadeSlideDown 0.5s ease-out;
140
+ }
141
+
142
+ @keyframes fadeSlideDown {
143
+ from { opacity: 0; transform: translateY(-12px); }
144
+ to { opacity: 1; transform: translateY(0); }
145
+ }
146
+
147
+ @keyframes fadeIn {
148
+ from { opacity: 0; }
149
+ to { opacity: 1; }
150
+ }
151
+
152
+ @keyframes fadeSlideUp {
153
+ from { opacity: 0; transform: translateY(8px); }
154
+ to { opacity: 1; transform: translateY(0); }
155
+ }
156
+
157
+ .header-row {
158
+ display: flex;
159
+ align-items: baseline;
160
+ gap: 16px;
161
+ }
162
+
163
+ header h1 {
164
+ font-family: var(--font-mono);
165
+ font-size: 1.3rem;
166
+ font-weight: 500;
167
+ color: var(--accent);
168
+ letter-spacing: -0.02em;
169
+ }
170
+
171
+ header h1 .cursor {
172
+ display: inline-block;
173
+ width: 2px;
174
+ height: 1.1em;
175
+ background: var(--accent);
176
+ margin-left: 2px;
177
+ vertical-align: text-bottom;
178
+ animation: blink 1.2s step-end infinite;
179
+ }
180
+
181
+ @keyframes blink {
182
+ 0%, 100% { opacity: 1; }
183
+ 50% { opacity: 0; }
184
+ }
185
+
186
+ header .subtitle {
187
+ font-family: var(--font-mono);
188
+ font-size: 0.75rem;
189
+ color: var(--text-dim);
190
+ letter-spacing: 0.08em;
191
+ text-transform: uppercase;
192
+ }
193
+
194
+ /* ── Stats bar ── */
195
+ .stats-bar {
196
+ display: flex;
197
+ gap: 32px;
198
+ padding: 14px 0;
199
+ font-size: 0.78rem;
200
+ color: var(--text-muted);
201
+ font-family: var(--font-mono);
202
+ border-top: 1px solid var(--border-subtle);
203
+ border-bottom: 1px solid var(--border-subtle);
204
+ margin-bottom: 28px;
205
+ animation: fadeIn 0.6s ease-out 0.1s both;
206
+ }
207
+
208
+ .stats-bar .stat-label {
209
+ color: var(--text-dim);
210
+ text-transform: uppercase;
211
+ font-size: 0.65rem;
212
+ letter-spacing: 0.1em;
213
+ margin-right: 6px;
214
+ }
215
+
216
+ .stats-bar .stat-value {
217
+ color: var(--text-secondary);
218
+ }
219
+
220
+ /* ── Search ── */
221
+ .search-section {
222
+ margin-bottom: 20px;
223
+ animation: fadeSlideUp 0.4s ease-out 0.15s both;
224
+ }
225
+
226
+ .search-wrapper {
227
+ position: relative;
228
+ }
229
+
230
+ .search-wrapper input {
231
+ width: 100%;
232
+ padding: 16px 48px 16px 48px;
233
+ background: var(--bg-input);
234
+ border: 1px solid var(--border);
235
+ border-radius: var(--radius);
236
+ color: var(--text-primary);
237
+ font-size: 0.95rem;
238
+ font-family: var(--font-mono);
239
+ font-weight: 400;
240
+ outline: none;
241
+ transition: border-color var(--transition), box-shadow var(--transition);
242
+ }
243
+
244
+ .search-wrapper input:focus {
245
+ border-color: var(--accent-dim);
246
+ box-shadow: 0 0 0 3px var(--accent-glow), inset 0 1px 4px rgba(0,0,0,0.3);
247
+ }
248
+
249
+ .search-wrapper input::placeholder {
250
+ color: var(--text-dim);
251
+ font-weight: 300;
252
+ }
253
+
254
+ .search-icon {
255
+ position: absolute;
256
+ left: 18px;
257
+ top: 50%;
258
+ transform: translateY(-50%);
259
+ color: var(--accent-dim);
260
+ font-family: var(--font-mono);
261
+ font-size: 1rem;
262
+ font-weight: 600;
263
+ pointer-events: none;
264
+ transition: color var(--transition);
265
+ }
266
+
267
+ .search-wrapper input:focus ~ .search-icon {
268
+ color: var(--accent);
269
+ }
270
+
271
+ .search-hint {
272
+ position: absolute;
273
+ right: 16px;
274
+ top: 50%;
275
+ transform: translateY(-50%);
276
+ color: var(--text-dim);
277
+ font-size: 0.7rem;
278
+ font-family: var(--font-mono);
279
+ background: var(--bg-card);
280
+ padding: 3px 8px;
281
+ border-radius: var(--radius-sm);
282
+ border: 1px solid var(--border);
283
+ line-height: 1;
284
+ transition: opacity var(--transition);
285
+ }
286
+
287
+ .search-wrapper input:focus ~ .search-hint {
288
+ opacity: 0;
289
+ }
290
+
291
+ /* ── Filters ── */
292
+ .filters {
293
+ display: flex;
294
+ gap: 8px;
295
+ flex-wrap: wrap;
296
+ margin-bottom: 24px;
297
+ animation: fadeSlideUp 0.4s ease-out 0.2s both;
298
+ }
299
+
300
+ .filters select {
301
+ padding: 8px 28px 8px 12px;
302
+ background: var(--bg-card);
303
+ border: 1px solid var(--border);
304
+ border-radius: var(--radius-sm);
305
+ color: var(--text-secondary);
306
+ font-size: 0.8rem;
307
+ font-family: var(--font-mono);
308
+ outline: none;
309
+ cursor: pointer;
310
+ min-width: 140px;
311
+ transition: border-color var(--transition);
312
+ -webkit-appearance: none;
313
+ appearance: none;
314
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='10' height='6'%3E%3Cpath d='M0 0l5 6 5-6z' fill='%235a584f'/%3E%3C/svg%3E");
315
+ background-repeat: no-repeat;
316
+ background-position: right 10px center;
317
+ }
318
+
319
+ .filters select:focus {
320
+ border-color: var(--accent-dim);
321
+ }
322
+
323
+ .filters select option {
324
+ background: var(--bg-card);
325
+ color: var(--text-primary);
326
+ }
327
+
328
+ .reindex-btn {
329
+ margin-left: auto;
330
+ padding: 8px 16px;
331
+ background: transparent;
332
+ border: 1px solid var(--border);
333
+ border-radius: var(--radius-sm);
334
+ color: var(--text-muted);
335
+ font-size: 0.75rem;
336
+ font-family: var(--font-mono);
337
+ letter-spacing: 0.04em;
338
+ text-transform: uppercase;
339
+ cursor: pointer;
340
+ transition: all var(--transition);
341
+ }
342
+
343
+ .reindex-btn:hover {
344
+ color: var(--accent);
345
+ border-color: var(--accent-dim);
346
+ background: var(--accent-glow);
347
+ }
348
+
349
+ .reindex-btn:disabled {
350
+ opacity: 0.4;
351
+ cursor: not-allowed;
352
+ }
353
+
354
+ /* ── View toggle ── */
355
+ .view-toggle {
356
+ display: flex;
357
+ gap: 0;
358
+ margin-bottom: 20px;
359
+ animation: fadeSlideUp 0.4s ease-out 0.25s both;
360
+ }
361
+
362
+ .view-toggle button {
363
+ padding: 7px 18px;
364
+ background: transparent;
365
+ border: 1px solid var(--border);
366
+ color: var(--text-muted);
367
+ font-size: 0.78rem;
368
+ font-family: var(--font-mono);
369
+ cursor: pointer;
370
+ transition: all var(--transition);
371
+ position: relative;
372
+ }
373
+
374
+ .view-toggle button:first-child {
375
+ border-radius: var(--radius-sm) 0 0 var(--radius-sm);
376
+ border-right: none;
377
+ }
378
+
379
+ .view-toggle button:last-child {
380
+ border-radius: 0 var(--radius-sm) var(--radius-sm) 0;
381
+ }
382
+
383
+ .view-toggle button.active {
384
+ color: var(--accent);
385
+ background: var(--accent-glow);
386
+ border-color: var(--accent-dim);
387
+ }
388
+
389
+ .view-toggle button.active + button {
390
+ border-left-color: var(--accent-dim);
391
+ }
392
+
393
+ /* ── Results info ── */
394
+ .results-info {
395
+ font-size: 0.75rem;
396
+ color: var(--text-dim);
397
+ margin-bottom: 16px;
398
+ font-family: var(--font-mono);
399
+ letter-spacing: 0.02em;
400
+ }
401
+
402
+ /* ── Cards ── */
403
+ .card {
404
+ background: var(--bg-card);
405
+ border: 1px solid var(--border);
406
+ border-radius: var(--radius);
407
+ padding: 18px 20px;
408
+ margin-bottom: 8px;
409
+ cursor: pointer;
410
+ transition: all var(--transition);
411
+ position: relative;
412
+ box-shadow: var(--shadow-card);
413
+ }
414
+
415
+ .card::before {
416
+ content: "";
417
+ position: absolute;
418
+ left: 0;
419
+ top: 12px;
420
+ bottom: 12px;
421
+ width: 2px;
422
+ background: var(--border);
423
+ border-radius: 1px;
424
+ transition: background var(--transition);
425
+ }
426
+
427
+ .card:hover {
428
+ background: var(--bg-card-hover);
429
+ box-shadow: var(--shadow-hover);
430
+ transform: translateY(-1px);
431
+ }
432
+
433
+ .card:hover::before {
434
+ background: var(--accent);
435
+ }
436
+
437
+ .card-header {
438
+ display: flex;
439
+ align-items: center;
440
+ gap: 8px;
441
+ flex-wrap: wrap;
442
+ margin-bottom: 10px;
443
+ padding-left: 12px;
444
+ }
445
+
446
+ .badge {
447
+ display: inline-flex;
448
+ align-items: center;
449
+ padding: 2px 8px;
450
+ border-radius: var(--radius-sm);
451
+ font-size: 0.65rem;
452
+ font-family: var(--font-mono);
453
+ font-weight: 500;
454
+ text-transform: uppercase;
455
+ letter-spacing: 0.06em;
456
+ line-height: 1.6;
457
+ }
458
+
459
+ .badge-role-user {
460
+ background: var(--badge-user-bg);
461
+ color: var(--badge-user);
462
+ border: 1px solid var(--badge-user);
463
+ border-color: transparent;
464
+ }
465
+
466
+ .badge-role-assistant {
467
+ background: var(--badge-assistant-bg);
468
+ color: var(--badge-assistant);
469
+ border: 1px solid var(--badge-assistant);
470
+ border-color: transparent;
471
+ }
472
+
473
+ .badge-project {
474
+ background: var(--badge-project-bg);
475
+ color: var(--badge-project);
476
+ }
477
+
478
+ .badge-branch {
479
+ background: var(--badge-branch-bg);
480
+ color: var(--badge-branch);
481
+ }
482
+
483
+ .card-snippet {
484
+ font-family: var(--font-mono);
485
+ font-size: 0.82rem;
486
+ color: var(--text-secondary);
487
+ line-height: 1.6;
488
+ margin-bottom: 10px;
489
+ padding-left: 12px;
490
+ white-space: pre-wrap;
491
+ word-break: break-word;
492
+ max-height: 110px;
493
+ overflow: hidden;
494
+ mask-image: linear-gradient(to bottom, #000 70%, transparent);
495
+ -webkit-mask-image: linear-gradient(to bottom, #000 70%, transparent);
496
+ }
497
+
498
+ .card-snippet mark {
499
+ background: var(--highlight-bg);
500
+ color: var(--highlight-text);
501
+ border-radius: 2px;
502
+ padding: 1px 3px;
503
+ font-weight: 500;
504
+ }
505
+
506
+ .card-summary {
507
+ font-size: 0.78rem;
508
+ color: var(--text-dim);
509
+ margin-bottom: 8px;
510
+ padding-left: 12px;
511
+ font-style: italic;
512
+ font-family: var(--font-body);
513
+ }
514
+
515
+ .card-meta {
516
+ display: flex;
517
+ align-items: center;
518
+ gap: 16px;
519
+ padding-left: 12px;
520
+ font-size: 0.7rem;
521
+ color: var(--text-dim);
522
+ font-family: var(--font-mono);
523
+ }
524
+
525
+ .card-meta span::before {
526
+ content: "";
527
+ display: inline-block;
528
+ width: 3px;
529
+ height: 3px;
530
+ background: var(--text-dim);
531
+ border-radius: 50%;
532
+ margin-right: 8px;
533
+ vertical-align: middle;
534
+ }
535
+
536
+ .card-meta span:first-child::before {
537
+ display: none;
538
+ }
539
+
540
+ .card-expanded {
541
+ margin-top: 14px;
542
+ padding: 14px 12px;
543
+ border-top: 1px solid var(--border);
544
+ font-family: var(--font-mono);
545
+ font-size: 0.82rem;
546
+ color: var(--text-secondary);
547
+ white-space: pre-wrap;
548
+ word-break: break-word;
549
+ max-height: 400px;
550
+ overflow-y: auto;
551
+ line-height: 1.6;
552
+ background: var(--bg-primary);
553
+ border-radius: 0 0 var(--radius) var(--radius);
554
+ margin-left: -20px;
555
+ margin-right: -20px;
556
+ margin-bottom: -18px;
557
+ padding: 16px 20px 16px 32px;
558
+ }
559
+
560
+ /* ── Session list cards ── */
561
+ .session-card {
562
+ background: var(--bg-card);
563
+ border: 1px solid var(--border);
564
+ border-radius: var(--radius);
565
+ padding: 16px 20px;
566
+ margin-bottom: 6px;
567
+ cursor: pointer;
568
+ transition: all var(--transition);
569
+ display: flex;
570
+ align-items: center;
571
+ gap: 14px;
572
+ box-shadow: var(--shadow-card);
573
+ }
574
+
575
+ .session-card:hover {
576
+ background: var(--bg-card-hover);
577
+ box-shadow: var(--shadow-hover);
578
+ transform: translateY(-1px);
579
+ }
580
+
581
+ .session-card .session-info {
582
+ flex: 1;
583
+ min-width: 0;
584
+ }
585
+
586
+ .session-card .session-title {
587
+ font-size: 0.88rem;
588
+ font-weight: 500;
589
+ color: var(--text-primary);
590
+ margin-bottom: 6px;
591
+ white-space: nowrap;
592
+ overflow: hidden;
593
+ text-overflow: ellipsis;
594
+ }
595
+
596
+ .session-card .session-meta {
597
+ display: flex;
598
+ align-items: center;
599
+ gap: 8px;
600
+ flex-wrap: wrap;
601
+ font-size: 0.72rem;
602
+ color: var(--text-muted);
603
+ font-family: var(--font-mono);
604
+ }
605
+
606
+ /* ── Session detail view ── */
607
+ .session-detail {
608
+ display: none;
609
+ }
610
+
611
+ .session-detail.active {
612
+ display: block;
613
+ animation: fadeSlideUp 0.3s ease-out;
614
+ }
615
+
616
+ .session-back {
617
+ display: inline-flex;
618
+ align-items: center;
619
+ gap: 6px;
620
+ padding: 7px 14px;
621
+ background: transparent;
622
+ border: 1px solid var(--border);
623
+ border-radius: var(--radius-sm);
624
+ color: var(--text-muted);
625
+ font-size: 0.78rem;
626
+ font-family: var(--font-mono);
627
+ cursor: pointer;
628
+ margin-bottom: 20px;
629
+ transition: all var(--transition);
630
+ }
631
+
632
+ .session-back:hover {
633
+ border-color: var(--accent-dim);
634
+ color: var(--accent);
635
+ text-decoration: none;
636
+ }
637
+
638
+ .session-header-detail {
639
+ margin-bottom: 24px;
640
+ padding: 20px 24px;
641
+ background: var(--bg-card);
642
+ border: 1px solid var(--border);
643
+ border-radius: var(--radius);
644
+ box-shadow: var(--shadow-card);
645
+ }
646
+
647
+ .session-header-detail h2 {
648
+ font-size: 0.85rem;
649
+ color: var(--text-secondary);
650
+ margin-bottom: 10px;
651
+ font-family: var(--font-mono);
652
+ font-weight: 400;
653
+ }
654
+
655
+ .session-header-detail .meta-row {
656
+ display: flex;
657
+ gap: 8px;
658
+ flex-wrap: wrap;
659
+ align-items: center;
660
+ font-size: 0.8rem;
661
+ color: var(--text-muted);
662
+ }
663
+
664
+ .resume-cmd {
665
+ display: flex;
666
+ align-items: center;
667
+ gap: 0;
668
+ margin-top: 14px;
669
+ background: var(--bg-primary);
670
+ border: 1px solid var(--border);
671
+ border-radius: var(--radius-sm);
672
+ overflow: hidden;
673
+ }
674
+
675
+ .resume-cmd code {
676
+ flex: 1;
677
+ padding: 8px 12px;
678
+ font-family: var(--font-mono);
679
+ font-size: 0.78rem;
680
+ color: var(--text-secondary);
681
+ white-space: nowrap;
682
+ overflow-x: auto;
683
+ user-select: all;
684
+ -webkit-user-select: all;
685
+ }
686
+
687
+ .resume-cmd button {
688
+ padding: 8px 14px;
689
+ background: var(--bg-card);
690
+ border: none;
691
+ border-left: 1px solid var(--border);
692
+ color: var(--text-muted);
693
+ font-family: var(--font-mono);
694
+ font-size: 0.72rem;
695
+ cursor: pointer;
696
+ transition: all var(--transition);
697
+ white-space: nowrap;
698
+ text-transform: uppercase;
699
+ letter-spacing: 0.06em;
700
+ }
701
+
702
+ .resume-cmd button:hover {
703
+ color: var(--accent);
704
+ background: var(--accent-glow);
705
+ }
706
+
707
+ .resume-cmd button.copied {
708
+ color: var(--success);
709
+ }
710
+
711
+ .message-item {
712
+ margin-bottom: 8px;
713
+ padding: 16px 20px 16px 22px;
714
+ background: var(--bg-card);
715
+ border: 1px solid var(--border);
716
+ border-radius: var(--radius);
717
+ border-left: 3px solid var(--border);
718
+ box-shadow: var(--shadow-card);
719
+ transition: border-left-color var(--transition);
720
+ }
721
+
722
+ .message-item.role-user {
723
+ border-left-color: var(--badge-user);
724
+ }
725
+
726
+ .message-item.role-assistant {
727
+ border-left-color: var(--badge-assistant);
728
+ }
729
+
730
+ .message-header {
731
+ display: flex;
732
+ align-items: center;
733
+ gap: 10px;
734
+ margin-bottom: 10px;
735
+ }
736
+
737
+ .message-header .timestamp {
738
+ font-size: 0.7rem;
739
+ color: var(--text-dim);
740
+ font-family: var(--font-mono);
741
+ }
742
+
743
+ .message-content {
744
+ font-family: var(--font-mono);
745
+ font-size: 0.82rem;
746
+ color: var(--text-secondary);
747
+ white-space: pre-wrap;
748
+ word-break: break-word;
749
+ line-height: 1.65;
750
+ }
751
+
752
+ /* ── Pagination ── */
753
+ .pagination {
754
+ display: flex;
755
+ justify-content: center;
756
+ align-items: center;
757
+ gap: 4px;
758
+ padding: 28px 0 48px;
759
+ }
760
+
761
+ .pagination button {
762
+ padding: 7px 16px;
763
+ background: transparent;
764
+ border: 1px solid var(--border);
765
+ border-radius: var(--radius-sm);
766
+ color: var(--text-muted);
767
+ font-size: 0.78rem;
768
+ font-family: var(--font-mono);
769
+ cursor: pointer;
770
+ transition: all var(--transition);
771
+ }
772
+
773
+ .pagination button:hover:not(:disabled) {
774
+ border-color: var(--accent-dim);
775
+ color: var(--accent);
776
+ background: var(--accent-glow);
777
+ }
778
+
779
+ .pagination button:disabled {
780
+ opacity: 0.2;
781
+ cursor: not-allowed;
782
+ }
783
+
784
+ .pagination .page-info {
785
+ font-size: 0.75rem;
786
+ color: var(--text-dim);
787
+ font-family: var(--font-mono);
788
+ padding: 0 14px;
789
+ }
790
+
791
+ /* ── Loading / empty states ── */
792
+ .loading, .empty-state {
793
+ text-align: center;
794
+ padding: 56px 0;
795
+ color: var(--text-dim);
796
+ font-size: 0.85rem;
797
+ font-family: var(--font-mono);
798
+ }
799
+
800
+ .loading::after {
801
+ content: "";
802
+ display: inline-block;
803
+ width: 14px;
804
+ height: 14px;
805
+ border: 1.5px solid var(--border);
806
+ border-top-color: var(--accent);
807
+ border-radius: 50%;
808
+ animation: spin 0.8s linear infinite;
809
+ margin-left: 10px;
810
+ vertical-align: middle;
811
+ }
812
+
813
+ @keyframes spin {
814
+ to { transform: rotate(360deg); }
815
+ }
816
+
817
+ .error-state {
818
+ text-align: center;
819
+ padding: 20px 24px;
820
+ color: var(--danger);
821
+ font-size: 0.82rem;
822
+ font-family: var(--font-mono);
823
+ background: var(--bg-card);
824
+ border: 1px solid var(--danger);
825
+ border-radius: var(--radius);
826
+ margin: 16px 0;
827
+ }
828
+
829
+ /* ── Toast ── */
830
+ .toast {
831
+ position: fixed;
832
+ bottom: 28px;
833
+ right: 28px;
834
+ padding: 12px 20px;
835
+ background: var(--bg-elevated);
836
+ border: 1px solid var(--border);
837
+ border-radius: var(--radius);
838
+ color: var(--text-secondary);
839
+ font-size: 0.82rem;
840
+ font-family: var(--font-mono);
841
+ z-index: 100;
842
+ opacity: 0;
843
+ transform: translateY(8px);
844
+ transition: all 0.3s cubic-bezier(0.25, 0.46, 0.45, 0.94);
845
+ pointer-events: none;
846
+ box-shadow: 0 8px 32px rgba(0,0,0,0.5);
847
+ }
848
+
849
+ .toast.visible {
850
+ opacity: 1;
851
+ transform: translateY(0);
852
+ }
853
+
854
+ .toast.toast-success {
855
+ border-color: var(--success);
856
+ border-left: 3px solid var(--success);
857
+ }
858
+
859
+ .toast.toast-error {
860
+ border-color: var(--danger);
861
+ border-left: 3px solid var(--danger);
862
+ }
863
+
864
+ /* ── Responsive ── */
865
+ @media (max-width: 600px) {
866
+ .container {
867
+ padding: 0 16px;
868
+ }
869
+ .filters {
870
+ flex-direction: column;
871
+ }
872
+ .filters select {
873
+ min-width: 100%;
874
+ }
875
+ .reindex-btn {
876
+ margin-left: 0;
877
+ }
878
+ .stats-bar {
879
+ flex-wrap: wrap;
880
+ gap: 12px 24px;
881
+ }
882
+ header {
883
+ padding: 32px 0 16px;
884
+ }
885
+ .header-row {
886
+ flex-direction: column;
887
+ gap: 4px;
888
+ }
889
+ }
890
+ </style>
891
+ </head>
892
+ <body>
893
+
894
+ <div class="container">
895
+ <header>
896
+ <div class="header-row">
897
+ <h1><a href="/" style="color:inherit;text-decoration:none">cc-log-viewer</a><span class="cursor"></span></h1>
898
+ <span class="subtitle">Claude Code session search</span>
899
+ </div>
900
+ </header>
901
+
902
+ <div class="stats-bar" id="stats-bar">
903
+ <span><span class="stat-label">Sessions</span><span class="stat-value" id="stat-sessions">--</span></span>
904
+ <span><span class="stat-label">Messages</span><span class="stat-value" id="stat-messages">--</span></span>
905
+ <span><span class="stat-label">Projects</span><span class="stat-value" id="stat-projects">--</span></span>
906
+ <span><span class="stat-label">Indexed</span><span class="stat-value" id="stat-last-indexed">--</span></span>
907
+ </div>
908
+
909
+ <div class="search-section">
910
+ <div class="search-wrapper">
911
+ <input type="text" id="search-input" placeholder="Search messages..." autocomplete="off" />
912
+ <span class="search-icon">&gt;</span>
913
+ <span class="search-hint">/</span>
914
+ </div>
915
+ </div>
916
+
917
+ <div class="filters">
918
+ <select id="filter-project">
919
+ <option value="">All projects</option>
920
+ </select>
921
+ <select id="filter-branch">
922
+ <option value="">All branches</option>
923
+ </select>
924
+ <select id="filter-role">
925
+ <option value="">All roles</option>
926
+ <option value="user">user</option>
927
+ <option value="assistant">assistant</option>
928
+ </select>
929
+ <button class="reindex-btn" id="reindex-btn">Reindex</button>
930
+ </div>
931
+
932
+ <div class="view-toggle">
933
+ <button id="view-search" class="active">Search</button>
934
+ <button id="view-sessions">Sessions</button>
935
+ </div>
936
+
937
+ <!-- Main content area -->
938
+ <div id="main-view">
939
+ <div id="results-info" class="results-info"></div>
940
+ <div id="results-container"></div>
941
+ <div id="pagination" class="pagination" style="display:none;">
942
+ <button id="page-prev">Prev</button>
943
+ <span class="page-info" id="page-info"></span>
944
+ <button id="page-next">Next</button>
945
+ </div>
946
+ </div>
947
+
948
+ <!-- Session detail view -->
949
+ <div id="session-detail" class="session-detail">
950
+ <a href="#" class="session-back" id="session-back">&lt;-- Back</a>
951
+ <div id="session-header-detail" class="session-header-detail"></div>
952
+ <div id="session-messages"></div>
953
+ </div>
954
+ </div>
955
+
956
+ <div class="toast" id="toast"></div>
957
+
958
+ <script>
959
+ (function () {
960
+ "use strict";
961
+
962
+ // --- State ---
963
+ var state = {
964
+ view: "search", // "search" | "sessions" | "session-detail"
965
+ query: "",
966
+ project: "",
967
+ branch: "",
968
+ role: "",
969
+ page: 1,
970
+ perPage: 100,
971
+ totalResults: 0,
972
+ expandedCard: null,
973
+ currentSessionId: null,
974
+ debounceTimer: null
975
+ };
976
+
977
+ // --- DOM refs ---
978
+ var searchInput = document.getElementById("search-input");
979
+ var filterProject = document.getElementById("filter-project");
980
+ var filterBranch = document.getElementById("filter-branch");
981
+ var filterRole = document.getElementById("filter-role");
982
+ var reindexBtn = document.getElementById("reindex-btn");
983
+ var viewSearchBtn = document.getElementById("view-search");
984
+ var viewSessionsBtn = document.getElementById("view-sessions");
985
+ var mainView = document.getElementById("main-view");
986
+ var sessionDetail = document.getElementById("session-detail");
987
+ var resultsInfo = document.getElementById("results-info");
988
+ var resultsContainer = document.getElementById("results-container");
989
+ var pagination = document.getElementById("pagination");
990
+ var pagePrev = document.getElementById("page-prev");
991
+ var pageNext = document.getElementById("page-next");
992
+ var pageInfo = document.getElementById("page-info");
993
+ var sessionBack = document.getElementById("session-back");
994
+ var toast = document.getElementById("toast");
995
+
996
+ // --- Utilities ---
997
+ function escapeHtml(str) {
998
+ var div = document.createElement("div");
999
+ div.textContent = str;
1000
+ return div.innerHTML;
1001
+ }
1002
+
1003
+ function formatTimestamp(ts) {
1004
+ if (!ts) return "--";
1005
+ try {
1006
+ var d = new Date(ts);
1007
+ return d.toLocaleString();
1008
+ } catch (e) {
1009
+ return ts;
1010
+ }
1011
+ }
1012
+
1013
+ function showToast(message, type) {
1014
+ toast.textContent = message;
1015
+ toast.className = "toast visible" + (type ? " toast-" + type : "");
1016
+ setTimeout(function () {
1017
+ toast.className = "toast";
1018
+ }, 3000);
1019
+ }
1020
+
1021
+ function totalPages() {
1022
+ return Math.max(1, Math.ceil(state.totalResults / state.perPage));
1023
+ }
1024
+
1025
+ // --- API ---
1026
+ function apiFetch(path, options) {
1027
+ return fetch(path, options).then(function (res) {
1028
+ if (!res.ok) throw new Error("HTTP " + res.status);
1029
+ return res.json();
1030
+ });
1031
+ }
1032
+
1033
+ function loadStats() {
1034
+ apiFetch("/api/stats").then(function (data) {
1035
+ document.getElementById("stat-sessions").textContent = data.sessions || data.session_count || 0;
1036
+ document.getElementById("stat-messages").textContent = data.messages || data.message_count || 0;
1037
+ document.getElementById("stat-projects").textContent = (data.projects || []).length;
1038
+ document.getElementById("stat-last-indexed").textContent = formatTimestamp(data.last_indexed || data.last_indexed_at);
1039
+
1040
+ // populate project filter
1041
+ var projects = data.projects || [];
1042
+ filterProject.innerHTML = '<option value="">All projects</option>';
1043
+ projects.forEach(function (p) {
1044
+ var opt = document.createElement("option");
1045
+ opt.value = p;
1046
+ opt.textContent = p;
1047
+ filterProject.appendChild(opt);
1048
+ });
1049
+ }).catch(function () {
1050
+ // silently fail for stats
1051
+ });
1052
+ }
1053
+
1054
+ function doSearch() {
1055
+ var params = new URLSearchParams();
1056
+ if (state.query) params.set("q", state.query);
1057
+ if (state.project) params.set("project", state.project);
1058
+ if (state.branch) params.set("branch", state.branch);
1059
+ if (state.role) params.set("role", state.role);
1060
+ params.set("page", state.page);
1061
+ params.set("per_page", state.perPage);
1062
+
1063
+ resultsContainer.innerHTML = '<div class="loading">Searching</div>';
1064
+ pagination.style.display = "none";
1065
+ resultsInfo.textContent = "";
1066
+
1067
+ apiFetch("/api/search?" + params.toString()).then(function (data) {
1068
+ state.totalResults = data.total || 0;
1069
+ renderSearchResults(data.results || [], data.total);
1070
+ }).catch(function (err) {
1071
+ resultsContainer.innerHTML = '<div class="error-state">Search failed: ' + escapeHtml(err.message) + '</div>';
1072
+ });
1073
+ }
1074
+
1075
+ function loadSessions() {
1076
+ var params = new URLSearchParams();
1077
+ if (state.project) params.set("project", state.project);
1078
+ if (state.branch) params.set("branch", state.branch);
1079
+ params.set("page", state.page);
1080
+ params.set("per_page", state.perPage);
1081
+
1082
+ resultsContainer.innerHTML = '<div class="loading">Loading sessions</div>';
1083
+ pagination.style.display = "none";
1084
+ resultsInfo.textContent = "";
1085
+
1086
+ apiFetch("/api/sessions?" + params.toString()).then(function (data) {
1087
+ state.totalResults = data.total || 0;
1088
+ renderSessionList(data.sessions || [], data.total);
1089
+ }).catch(function (err) {
1090
+ resultsContainer.innerHTML = '<div class="error-state">Failed to load sessions: ' + escapeHtml(err.message) + '</div>';
1091
+ });
1092
+ }
1093
+
1094
+ function loadSessionDetail(sessionId) {
1095
+ state.currentSessionId = sessionId;
1096
+ state.view = "session-detail";
1097
+ mainView.style.display = "none";
1098
+ sessionDetail.classList.add("active");
1099
+ history.pushState({ session: sessionId }, "", "/sessions/" + sessionId);
1100
+
1101
+ var headerEl = document.getElementById("session-header-detail");
1102
+ var messagesEl = document.getElementById("session-messages");
1103
+ headerEl.innerHTML = '<div class="loading">Loading session</div>';
1104
+ messagesEl.innerHTML = "";
1105
+
1106
+ apiFetch("/api/sessions/" + encodeURIComponent(sessionId)).then(function (data) {
1107
+ var session = data.session || {};
1108
+ var messages = data.messages || [];
1109
+
1110
+ var projectDir = session.project_path || '';
1111
+ var resumeCmd = projectDir
1112
+ ? 'cd ' + projectDir + ' && claude --resume ' + sessionId
1113
+ : 'claude --resume ' + sessionId;
1114
+ headerEl.innerHTML =
1115
+ '<h2>' + escapeHtml(session.summary || sessionId) + '</h2>' +
1116
+ '<div class="meta-row">' +
1117
+ (session.project_name ? '<span class="badge badge-project">' + escapeHtml(session.project_name) + '</span>' : '') +
1118
+ (session.git_branch ? '<span class="badge badge-branch">' + escapeHtml(session.git_branch) + '</span>' : '') +
1119
+ '<span style="font-family:var(--font-mono);font-size:0.72rem;color:var(--text-dim)">' + escapeHtml(sessionId) + '</span>' +
1120
+ '</div>' +
1121
+ '<div class="resume-cmd">' +
1122
+ '<code>' + escapeHtml(resumeCmd) + '</code>' +
1123
+ '<button id="copy-resume-cmd" data-cmd="' + escapeHtml(resumeCmd) + '">Copy</button>' +
1124
+ '</div>';
1125
+
1126
+ var copyBtn = document.getElementById("copy-resume-cmd");
1127
+ if (copyBtn) {
1128
+ copyBtn.addEventListener("click", function () {
1129
+ navigator.clipboard.writeText(copyBtn.getAttribute("data-cmd")).then(function () {
1130
+ copyBtn.textContent = "Copied";
1131
+ copyBtn.classList.add("copied");
1132
+ setTimeout(function () {
1133
+ copyBtn.textContent = "Copy";
1134
+ copyBtn.classList.remove("copied");
1135
+ }, 2000);
1136
+ });
1137
+ });
1138
+ }
1139
+
1140
+ if (messages.length === 0) {
1141
+ messagesEl.innerHTML = '<div class="empty-state">No messages in this session.</div>';
1142
+ return;
1143
+ }
1144
+
1145
+ var html = "";
1146
+ messages.forEach(function (msg, i) {
1147
+ var roleClass = msg.role === "user" ? "role-user" : "role-assistant";
1148
+ var badgeClass = msg.role === "user" ? "badge-role-user" : "badge-role-assistant";
1149
+ html +=
1150
+ '<div class="message-item ' + roleClass + '" style="animation:fadeSlideUp 0.2s ease-out ' + (i * 0.03) + 's both">' +
1151
+ '<div class="message-header">' +
1152
+ '<span class="badge ' + badgeClass + '">' + escapeHtml(msg.role || "unknown") + '</span>' +
1153
+ '<span class="timestamp">' + escapeHtml(formatTimestamp(msg.timestamp)) + '</span>' +
1154
+ '</div>' +
1155
+ '<div class="message-content">' + escapeHtml(msg.content || "") + '</div>' +
1156
+ '</div>';
1157
+ });
1158
+ messagesEl.innerHTML = html;
1159
+ }).catch(function (err) {
1160
+ headerEl.innerHTML = '<div class="error-state">Failed to load session: ' + escapeHtml(err.message) + '</div>';
1161
+ });
1162
+ }
1163
+
1164
+ // --- Rendering ---
1165
+ function renderSearchResults(results, total) {
1166
+ if (results.length === 0) {
1167
+ resultsContainer.innerHTML = '<div class="empty-state">No results found.</div>';
1168
+ pagination.style.display = "none";
1169
+ resultsInfo.textContent = "0 results";
1170
+ return;
1171
+ }
1172
+
1173
+ resultsInfo.textContent = total + " result" + (total !== 1 ? "s" : "") + " found";
1174
+
1175
+ var html = "";
1176
+ results.forEach(function (r, idx) {
1177
+ var roleClass = r.role === "user" ? "badge-role-user" : "badge-role-assistant";
1178
+ var snippet = r.snippet || truncate(r.content, 200);
1179
+ html +=
1180
+ '<div class="card" data-index="' + idx + '" data-session-id="' + escapeHtml(r.session_id || "") + '" style="animation:fadeSlideUp 0.25s ease-out ' + (idx * 0.04) + 's both">' +
1181
+ '<div class="card-header">' +
1182
+ '<span class="badge ' + roleClass + '">' + escapeHtml(r.role || "unknown") + '</span>' +
1183
+ (r.project_name ? '<span class="badge badge-project">' + escapeHtml(r.project_name) + '</span>' : '') +
1184
+ (r.git_branch ? '<span class="badge badge-branch">' + escapeHtml(r.git_branch) + '</span>' : '') +
1185
+ '</div>' +
1186
+ '<div class="card-snippet">' + highlightSnippet(snippet) + '</div>' +
1187
+ (r.summary ? '<div class="card-summary">' + escapeHtml(r.summary) + '</div>' : '') +
1188
+ '<div class="card-meta">' +
1189
+ '<span>' + escapeHtml(formatTimestamp(r.timestamp)) + '</span>' +
1190
+ '<span>' + escapeHtml(truncate(r.session_id || "", 16)) + '</span>' +
1191
+ '</div>' +
1192
+ '<div class="card-expanded" id="expanded-' + idx + '" style="display:none;"></div>' +
1193
+ '</div>';
1194
+ });
1195
+ resultsContainer.innerHTML = html;
1196
+
1197
+ // Card click handlers
1198
+ var cards = resultsContainer.querySelectorAll(".card");
1199
+ cards.forEach(function (card) {
1200
+ card.addEventListener("click", function (e) {
1201
+ var idx = parseInt(card.getAttribute("data-index"), 10);
1202
+ var expandedEl = document.getElementById("expanded-" + idx);
1203
+ var sessionId = card.getAttribute("data-session-id");
1204
+
1205
+ // If clicking with meta/ctrl key, open session detail
1206
+ if (e.metaKey || e.ctrlKey) {
1207
+ if (sessionId) loadSessionDetail(sessionId);
1208
+ return;
1209
+ }
1210
+
1211
+ // Toggle expanded content
1212
+ if (expandedEl.style.display === "none") {
1213
+ expandedEl.style.display = "block";
1214
+ expandedEl.textContent = results[idx].content || "(no content)";
1215
+ // Add a link to session detail
1216
+ var link = document.createElement("a");
1217
+ link.href = "#";
1218
+ link.textContent = "[View full session]";
1219
+ link.style.display = "block";
1220
+ link.style.marginTop = "8px";
1221
+ link.style.fontSize = "0.78rem";
1222
+ link.addEventListener("click", function (ev) {
1223
+ ev.preventDefault();
1224
+ ev.stopPropagation();
1225
+ if (sessionId) loadSessionDetail(sessionId);
1226
+ });
1227
+ expandedEl.appendChild(link);
1228
+ } else {
1229
+ expandedEl.style.display = "none";
1230
+ }
1231
+ });
1232
+ });
1233
+
1234
+ renderPagination();
1235
+ }
1236
+
1237
+ function renderSessionList(sessions, total) {
1238
+ if (sessions.length === 0) {
1239
+ resultsContainer.innerHTML = '<div class="empty-state">No sessions found.</div>';
1240
+ pagination.style.display = "none";
1241
+ resultsInfo.textContent = "0 sessions";
1242
+ return;
1243
+ }
1244
+
1245
+ resultsInfo.textContent = total + " session" + (total !== 1 ? "s" : "");
1246
+
1247
+ var html = "";
1248
+ sessions.forEach(function (s, idx) {
1249
+ html +=
1250
+ '<div class="session-card" data-session-id="' + escapeHtml(s.session_id || s.id || "") + '" style="animation:fadeSlideUp 0.25s ease-out ' + (idx * 0.03) + 's both">' +
1251
+ '<div class="session-info">' +
1252
+ '<div class="session-title">' +
1253
+ escapeHtml(s.summary || s.session_id || s.id || "Untitled session") +
1254
+ '</div>' +
1255
+ '<div class="session-meta">' +
1256
+ (s.project_name ? '<span class="badge badge-project">' + escapeHtml(s.project_name) + '</span> ' : '') +
1257
+ (s.git_branch ? '<span class="badge badge-branch">' + escapeHtml(s.git_branch) + '</span> ' : '') +
1258
+ '<span>' + escapeHtml(formatTimestamp(s.timestamp || s.created_at || s.created)) + '</span>' +
1259
+ '</div>' +
1260
+ '</div>' +
1261
+ '</div>';
1262
+ });
1263
+ resultsContainer.innerHTML = html;
1264
+
1265
+ var sessionCards = resultsContainer.querySelectorAll(".session-card");
1266
+ sessionCards.forEach(function (card) {
1267
+ card.addEventListener("click", function () {
1268
+ var sid = card.getAttribute("data-session-id");
1269
+ if (sid) loadSessionDetail(sid);
1270
+ });
1271
+ });
1272
+
1273
+ renderPagination();
1274
+ }
1275
+
1276
+ function renderPagination() {
1277
+ var tp = totalPages();
1278
+ if (tp <= 1) {
1279
+ pagination.style.display = "none";
1280
+ return;
1281
+ }
1282
+ pagination.style.display = "flex";
1283
+ pagePrev.disabled = state.page <= 1;
1284
+ pageNext.disabled = state.page >= tp;
1285
+ pageInfo.textContent = "Page " + state.page + " / " + tp;
1286
+ }
1287
+
1288
+ function highlightSnippet(text) {
1289
+ // If the API returns <mark> tags in the snippet, pass them through.
1290
+ // Otherwise, just escape.
1291
+ if (text.indexOf("<mark>") !== -1) {
1292
+ // Escape everything except <mark> and </mark>
1293
+ var parts = text.split(/(<\/?mark>)/g);
1294
+ return parts.map(function (part) {
1295
+ if (part === "<mark>" || part === "</mark>") return part;
1296
+ return escapeHtml(part);
1297
+ }).join("");
1298
+ }
1299
+ return escapeHtml(text);
1300
+ }
1301
+
1302
+ function truncate(str, len) {
1303
+ if (!str) return "";
1304
+ if (str.length <= len) return str;
1305
+ return str.substring(0, len) + "...";
1306
+ }
1307
+
1308
+ // --- View switching ---
1309
+ function switchView(view) {
1310
+ state.view = view;
1311
+ state.page = 1;
1312
+ state.expandedCard = null;
1313
+
1314
+ viewSearchBtn.classList.toggle("active", view === "search");
1315
+ viewSessionsBtn.classList.toggle("active", view === "sessions");
1316
+
1317
+ mainView.style.display = view === "session-detail" ? "none" : "block";
1318
+ sessionDetail.classList.toggle("active", view === "session-detail");
1319
+
1320
+ if (view === "search") {
1321
+ doSearch();
1322
+ } else if (view === "sessions") {
1323
+ loadSessions();
1324
+ }
1325
+ }
1326
+
1327
+ // --- Event handlers ---
1328
+ searchInput.addEventListener("input", function () {
1329
+ clearTimeout(state.debounceTimer);
1330
+ state.debounceTimer = setTimeout(function () {
1331
+ state.query = searchInput.value.trim();
1332
+ state.page = 1;
1333
+ // If in session-detail view, return to search view first
1334
+ if (state.view === "session-detail") {
1335
+ mainView.style.display = "block";
1336
+ sessionDetail.classList.remove("active");
1337
+ state.view = "search";
1338
+ viewSearchBtn.classList.add("active");
1339
+ viewSessionsBtn.classList.remove("active");
1340
+ }
1341
+ if (state.view === "search") doSearch();
1342
+ }, 300);
1343
+ });
1344
+
1345
+ function returnFromDetailIfNeeded() {
1346
+ if (state.view === "session-detail") {
1347
+ mainView.style.display = "block";
1348
+ sessionDetail.classList.remove("active");
1349
+ state.view = "search";
1350
+ viewSearchBtn.classList.add("active");
1351
+ viewSessionsBtn.classList.remove("active");
1352
+ }
1353
+ }
1354
+
1355
+ filterProject.addEventListener("change", function () {
1356
+ state.project = filterProject.value;
1357
+ state.page = 1;
1358
+ returnFromDetailIfNeeded();
1359
+ if (state.view === "search") doSearch();
1360
+ else if (state.view === "sessions") loadSessions();
1361
+ });
1362
+
1363
+ filterBranch.addEventListener("change", function () {
1364
+ state.branch = filterBranch.value;
1365
+ state.page = 1;
1366
+ returnFromDetailIfNeeded();
1367
+ if (state.view === "search") doSearch();
1368
+ else if (state.view === "sessions") loadSessions();
1369
+ });
1370
+
1371
+ filterRole.addEventListener("change", function () {
1372
+ state.role = filterRole.value;
1373
+ state.page = 1;
1374
+ returnFromDetailIfNeeded();
1375
+ if (state.view === "search") doSearch();
1376
+ });
1377
+
1378
+ pagePrev.addEventListener("click", function () {
1379
+ if (state.page > 1) {
1380
+ state.page--;
1381
+ if (state.view === "search") doSearch();
1382
+ else if (state.view === "sessions") loadSessions();
1383
+ }
1384
+ });
1385
+
1386
+ pageNext.addEventListener("click", function () {
1387
+ if (state.page < totalPages()) {
1388
+ state.page++;
1389
+ if (state.view === "search") doSearch();
1390
+ else if (state.view === "sessions") loadSessions();
1391
+ }
1392
+ });
1393
+
1394
+ viewSearchBtn.addEventListener("click", function () {
1395
+ if (state.view !== "search") switchView("search");
1396
+ });
1397
+
1398
+ viewSessionsBtn.addEventListener("click", function () {
1399
+ if (state.view !== "sessions") switchView("sessions");
1400
+ });
1401
+
1402
+ function goBackFromDetail() {
1403
+ state.view = state.query ? "search" : "sessions";
1404
+ mainView.style.display = "block";
1405
+ sessionDetail.classList.remove("active");
1406
+ if (state.view === "search") doSearch();
1407
+ else loadSessions();
1408
+ viewSearchBtn.classList.toggle("active", state.view === "search");
1409
+ viewSessionsBtn.classList.toggle("active", state.view === "sessions");
1410
+ }
1411
+
1412
+ sessionBack.addEventListener("click", function (e) {
1413
+ e.preventDefault();
1414
+ history.pushState(null, "", "/");
1415
+ goBackFromDetail();
1416
+ });
1417
+
1418
+ window.addEventListener("popstate", function () {
1419
+ var match = window.location.pathname.match(/^\/sessions\/(.+)$/);
1420
+ if (match) {
1421
+ loadSessionDetail(decodeURIComponent(match[1]));
1422
+ } else if (state.view === "session-detail") {
1423
+ goBackFromDetail();
1424
+ }
1425
+ });
1426
+
1427
+ reindexBtn.addEventListener("click", function () {
1428
+ reindexBtn.disabled = true;
1429
+ reindexBtn.textContent = "Reindexing...";
1430
+ fetch("/api/reindex", { method: "POST" })
1431
+ .then(function (res) {
1432
+ if (!res.ok) throw new Error("HTTP " + res.status);
1433
+ return res.json();
1434
+ })
1435
+ .then(function () {
1436
+ showToast("Reindex complete", "success");
1437
+ loadStats();
1438
+ if (state.view === "search") doSearch();
1439
+ else if (state.view === "sessions") loadSessions();
1440
+ })
1441
+ .catch(function (err) {
1442
+ showToast("Reindex failed: " + err.message, "error");
1443
+ })
1444
+ .finally(function () {
1445
+ reindexBtn.disabled = false;
1446
+ reindexBtn.textContent = "Reindex";
1447
+ });
1448
+ });
1449
+
1450
+ // Keyboard shortcut: "/" to focus search
1451
+ document.addEventListener("keydown", function (e) {
1452
+ if (e.key === "/" && document.activeElement !== searchInput) {
1453
+ var tag = (document.activeElement || {}).tagName;
1454
+ if (tag === "INPUT" || tag === "SELECT" || tag === "TEXTAREA") return;
1455
+ e.preventDefault();
1456
+ searchInput.focus();
1457
+ }
1458
+ // Escape to blur search
1459
+ if (e.key === "Escape" && document.activeElement === searchInput) {
1460
+ searchInput.blur();
1461
+ }
1462
+ });
1463
+
1464
+ // --- Periodic reindex ---
1465
+ var REINDEX_INTERVAL = 3 * 60 * 1000; // 3 minutes
1466
+ function autoReindex() {
1467
+ fetch("/api/reindex", { method: "POST" })
1468
+ .then(function (res) { return res.ok ? res.json() : null; })
1469
+ .then(function () {
1470
+ loadStats();
1471
+ if (state.view === "search") doSearch();
1472
+ else if (state.view === "sessions") loadSessions();
1473
+ })
1474
+ .catch(function () {});
1475
+ }
1476
+ setInterval(autoReindex, REINDEX_INTERVAL);
1477
+
1478
+ // --- Init ---
1479
+ autoReindex();
1480
+ loadStats();
1481
+ var initMatch = window.location.pathname.match(/^\/sessions\/(.+)$/);
1482
+ if (initMatch) {
1483
+ loadSessionDetail(decodeURIComponent(initMatch[1]));
1484
+ } else {
1485
+ doSearch();
1486
+ }
1487
+ })();
1488
+ </script>
1489
+ </body>
1490
+ </html>