code-data-ark 2.0.2__py3-none-any.whl

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.
cda/ui/web.py ADDED
@@ -0,0 +1,2848 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Code Data Ark Intelligence Portal — Complete Edition
4
+ Light-themed web UI with comprehensive CLI command access.
5
+ All 40+ CLI commands accessible as browser UI pages instead of terminal.
6
+ """
7
+
8
+ import json
9
+ import sqlite3
10
+ import threading
11
+ import time
12
+ import traceback
13
+ import subprocess
14
+ import socket
15
+ from typing import Any, Dict
16
+ from pathlib import Path
17
+ from datetime import datetime
18
+ from wsgiref.simple_server import make_server, WSGIServer
19
+ from urllib.parse import parse_qs
20
+ from cda.kernel.pmf_kernel import PMFKernel
21
+
22
+ # Get DB path relative to this file
23
+ PACKAGE_DIR = Path(__file__).resolve().parent
24
+ LOCAL_DIR = PACKAGE_DIR.parent.parent.parent / "local"
25
+ DB_PATH = LOCAL_DIR / "data" / "cda.db"
26
+ kernel = PMFKernel()
27
+
28
+ # ─────────────────────────────────────────────
29
+ # Light Theme CSS with all components
30
+ # ─────────────────────────────────────────────
31
+
32
+ STYLE_CSS = """
33
+ :root {
34
+ --bg-primary: #f8fafc;
35
+ --bg-secondary: #eef2ff;
36
+ --bg-tertiary: #dbeafe;
37
+ --text-primary: #1e293b;
38
+ --text-secondary: #475569;
39
+ --text-tertiary: #64748b;
40
+ --accent: #0ea5e9;
41
+ --accent-hover: #0284c7;
42
+ --danger: #ef4444;
43
+ --success: #10b981;
44
+ --warning: #f59e0b;
45
+ --border: #cbd5e1;
46
+ --input-bg: #ffffff;
47
+ --input-border: #cbd5e1;
48
+ --input-focus: #0ea5e9;
49
+ --shadow: 0 1px 3px rgba(0,0,0,0.1);
50
+ --shadow-md: 0 4px 6px rgba(0,0,0,0.1);
51
+ --transition: all 0.2s ease-in-out;
52
+ }
53
+
54
+ * { margin: 0; padding: 0; box-sizing: border-box; }
55
+ html, body { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
56
+ body {
57
+ background: var(--bg-primary);
58
+ color: var(--text-primary);
59
+ overflow-x: hidden;
60
+ }
61
+
62
+ #root {
63
+ display: flex;
64
+ height: 100vh;
65
+ }
66
+
67
+ .sidebar {
68
+ width: 240px;
69
+ background: #ffffff;
70
+ border-right: 1px solid var(--border);
71
+ overflow-y: auto;
72
+ padding: 20px 0;
73
+ }
74
+
75
+ .content {
76
+ flex: 1;
77
+ min-width: 0;
78
+ overflow: auto;
79
+ padding: 24px;
80
+ }
81
+
82
+ .sidebar-header {
83
+ padding: 0 20px 20px;
84
+ border-bottom: 1px solid var(--border);
85
+ margin-bottom: 10px;
86
+ }
87
+
88
+ .sidebar-title {
89
+ font-weight: 700;
90
+ font-size: 14px;
91
+ color: var(--text-secondary);
92
+ text-transform: uppercase;
93
+ letter-spacing: 0.5px;
94
+ }
95
+
96
+ .nav-group {
97
+ margin-bottom: 15px;
98
+ }
99
+
100
+ .nav-group-title {
101
+ padding: 8px 20px;
102
+ font-size: 11px;
103
+ font-weight: 600;
104
+ color: var(--text-tertiary);
105
+ text-transform: uppercase;
106
+ letter-spacing: 0.5px;
107
+ margin-top: 10px;
108
+ }
109
+
110
+ .nav-item {
111
+ padding: 10px 20px;
112
+ cursor: pointer;
113
+ color: var(--text-secondary);
114
+ font-size: 13px;
115
+ transition: var(--transition);
116
+ border-left: 3px solid transparent;
117
+ display: flex;
118
+ align-items: center;
119
+ }
120
+
121
+ .nav-item .icon {
122
+ display: inline-flex;
123
+ align-items: center;
124
+ justify-content: center;
125
+ width: 16px;
126
+ height: 16px;
127
+ margin-right: 10px;
128
+ stroke: currentColor;
129
+ fill: none;
130
+ }
131
+
132
+ .nav-item:hover {
133
+ background: var(--bg-secondary);
134
+ color: var(--accent);
135
+ }
136
+
137
+ .nav-item.active {
138
+ background: var(--bg-tertiary);
139
+ color: var(--accent);
140
+ border-left-color: var(--accent);
141
+ font-weight: 600;
142
+ }
143
+
144
+ .page-header {
145
+ display: flex;
146
+ justify-content: space-between;
147
+ align-items: flex-end;
148
+ gap: 16px;
149
+ margin-bottom: 20px;
150
+ }
151
+
152
+ .page-title {
153
+ font-size: 22px;
154
+ font-weight: 700;
155
+ color: var(--text-primary);
156
+ }
157
+
158
+ .page-subtitle {
159
+ color: var(--text-secondary);
160
+ font-size: 13px;
161
+ line-height: 1.4;
162
+ }
163
+
164
+ .drawer {
165
+ position: fixed;
166
+ inset: 0;
167
+ z-index: 1000;
168
+ display: flex;
169
+ pointer-events: none;
170
+ opacity: 0;
171
+ visibility: hidden;
172
+ transition: opacity 0.3s ease, backdrop-filter 0.3s ease;
173
+ backdrop-filter: blur(0px);
174
+ }
175
+
176
+ .drawer.open {
177
+ pointer-events: auto;
178
+ opacity: 1;
179
+ visibility: visible;
180
+ backdrop-filter: blur(4px);
181
+ }
182
+
183
+ .drawer-backdrop {
184
+ position: absolute;
185
+ inset: 0;
186
+ background: rgba(15, 23, 42, 0.6);
187
+ cursor: pointer;
188
+ }
189
+
190
+ .drawer-panel {
191
+ position: absolute;
192
+ inset: 40px;
193
+ top: 40px;
194
+ bottom: 40px;
195
+ background: #ffffff;
196
+ border-radius: 12px;
197
+ box-shadow: 0 20px 60px rgba(15, 23, 42, 0.3);
198
+ transform: scale(0.95) translateY(20px);
199
+ transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
200
+ display: flex;
201
+ flex-direction: column;
202
+ overflow: hidden;
203
+ }
204
+
205
+ .drawer.open .drawer-panel {
206
+ transform: scale(1) translateY(0);
207
+ }
208
+
209
+ .drawer-header {
210
+ padding: 24px 24px 0;
211
+ display: flex;
212
+ justify-content: space-between;
213
+ align-items: flex-start;
214
+ gap: 16px;
215
+ flex-shrink: 0;
216
+ }
217
+
218
+ .drawer-title {
219
+ display: flex;
220
+ flex-direction: column;
221
+ gap: 4px;
222
+ flex: 1;
223
+ }
224
+
225
+ .drawer-title .title {
226
+ font-size: 20px;
227
+ font-weight: 700;
228
+ color: var(--text-primary);
229
+ letter-spacing: -0.3px;
230
+ }
231
+
232
+ .drawer-title .subtitle {
233
+ color: var(--text-secondary);
234
+ font-size: 13px;
235
+ font-weight: 500;
236
+ }
237
+
238
+ .drawer-tabs {
239
+ display: flex;
240
+ gap: 8px;
241
+ padding: 16px 24px 6px;
242
+ border-bottom: 1px solid var(--border);
243
+ overflow-x: auto;
244
+ flex-shrink: 0;
245
+ }
246
+
247
+ .drawer-tab {
248
+ padding: 12px 18px 14px;
249
+ border-radius: 8px 8px 0 0;
250
+ background: transparent;
251
+ color: var(--text-tertiary);
252
+ text-align: center;
253
+ font-size: 13px;
254
+ font-weight: 600;
255
+ cursor: pointer;
256
+ transition: var(--transition);
257
+ white-space: nowrap;
258
+ border-bottom: 3px solid transparent;
259
+ margin-bottom: 0;
260
+ }
261
+
262
+ .drawer-tab:hover {
263
+ color: var(--text-secondary);
264
+ background: rgba(236, 244, 255, 0.8);
265
+ }
266
+
267
+ .drawer-tab.active {
268
+ color: var(--accent);
269
+ box-shadow: inset 0 -3px 0 0 var(--accent);
270
+ background: transparent;
271
+ }
272
+
273
+ .drawer-list {
274
+ list-style: none;
275
+ margin: 0;
276
+ padding: 0;
277
+ display: flex;
278
+ flex-direction: column;
279
+ gap: 12px;
280
+ }
281
+
282
+ .drawer-list li {
283
+ background: var(--bg-secondary);
284
+ border-radius: 8px;
285
+ padding: 12px;
286
+ border-left: 3px solid var(--accent);
287
+ }
288
+
289
+ .drawer-list li strong {
290
+ display: block;
291
+ color: var(--text-primary);
292
+ font-weight: 600;
293
+ margin-bottom: 4px;
294
+ }
295
+
296
+ .drawer-list li span {
297
+ display: block;
298
+ color: var(--text-secondary);
299
+ font-size: 12px;
300
+ word-break: break-word;
301
+ }
302
+
303
+ .drawer-close {
304
+ border: none;
305
+ background: transparent;
306
+ color: var(--text-tertiary);
307
+ font-size: 28px;
308
+ cursor: pointer;
309
+ line-height: 1;
310
+ width: 40px;
311
+ height: 40px;
312
+ display: flex;
313
+ align-items: center;
314
+ justify-content: center;
315
+ border-radius: 6px;
316
+ transition: var(--transition);
317
+ flex-shrink: 0;
318
+ }
319
+
320
+ .drawer-close:hover {
321
+ background: var(--bg-secondary);
322
+ color: var(--text-primary);
323
+ }
324
+
325
+ .drawer-body {
326
+ padding: 24px;
327
+ overflow-y: auto;
328
+ flex: 1;
329
+ min-height: 0;
330
+ }
331
+
332
+ .drawer-section {
333
+ margin-bottom: 28px;
334
+ }
335
+
336
+ .drawer-section:last-child {
337
+ margin-bottom: 0;
338
+ }
339
+
340
+ .drawer-section h3 {
341
+ margin-bottom: 14px;
342
+ font-size: 12px;
343
+ text-transform: uppercase;
344
+ letter-spacing: 0.6px;
345
+ color: var(--text-secondary);
346
+ font-weight: 700;
347
+ }
348
+
349
+ .chat-bubble {
350
+ border-radius: 12px;
351
+ padding: 14px 16px;
352
+ background: var(--bg-secondary);
353
+ margin-bottom: 12px;
354
+ line-height: 1.6;
355
+ border-left: 3px solid var(--border);
356
+ }
357
+
358
+ .chat-bubble.assistant {
359
+ background: var(--bg-tertiary);
360
+ border-left-color: var(--accent);
361
+ }
362
+
363
+ .chat-meta {
364
+ margin-bottom: 8px;
365
+ color: var(--text-secondary);
366
+ font-size: 11px;
367
+ font-weight: 600;
368
+ }
369
+
370
+ .chat-body {
371
+ white-space: pre-wrap;
372
+ word-wrap: break-word;
373
+ color: var(--text-primary);
374
+ }
375
+
376
+ .card-row {
377
+ display: flex;
378
+ justify-content: space-between;
379
+ gap: 12px;
380
+ margin-bottom: 12px;
381
+ color: var(--text-secondary);
382
+ font-size: 13px;
383
+ align-items: center;
384
+ padding: 8px 0;
385
+ border-bottom: 1px solid var(--border);
386
+ }
387
+
388
+ .card-row:last-child {
389
+ border-bottom: none;
390
+ }
391
+
392
+ .page-title {
393
+ font-size: 28px;
394
+ font-weight: 700;
395
+ color: var(--text-primary);
396
+ margin-bottom: 5px;
397
+ }
398
+
399
+ .page-subtitle {
400
+ font-size: 14px;
401
+ color: var(--text-secondary);
402
+ }
403
+
404
+ .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; margin-bottom: 20px; }
405
+ .grid-3 { display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 20px; margin-bottom: 20px; }
406
+ .grid-4 { display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 20px; margin-bottom: 20px; }
407
+
408
+ .card {
409
+ background: #ffffff;
410
+ border: 1px solid var(--border);
411
+ border-radius: 8px;
412
+ padding: 20px;
413
+ box-shadow: var(--shadow);
414
+ transition: var(--transition);
415
+ }
416
+
417
+ .card:hover {
418
+ box-shadow: var(--shadow-md);
419
+ transform: translateY(-2px);
420
+ }
421
+
422
+ .card-header {
423
+ font-weight: 600;
424
+ font-size: 14px;
425
+ color: var(--text-secondary);
426
+ text-transform: uppercase;
427
+ letter-spacing: 0.5px;
428
+ margin-bottom: 12px;
429
+ }
430
+
431
+ .card-value {
432
+ font-size: 32px;
433
+ font-weight: 700;
434
+ color: var(--accent);
435
+ margin-bottom: 8px;
436
+ }
437
+
438
+ .card-label {
439
+ font-size: 13px;
440
+ color: var(--text-tertiary);
441
+ }
442
+
443
+ .form-group {
444
+ margin-bottom: 15px;
445
+ }
446
+
447
+ .form-label {
448
+ display: block;
449
+ font-weight: 600;
450
+ font-size: 13px;
451
+ margin-bottom: 6px;
452
+ color: var(--text-secondary);
453
+ text-transform: uppercase;
454
+ letter-spacing: 0.5px;
455
+ }
456
+
457
+ .form-input, .form-select, .form-textarea {
458
+ width: 100%;
459
+ padding: 10px 12px;
460
+ border: 1px solid var(--input-border);
461
+ border-radius: 6px;
462
+ background: var(--input-bg);
463
+ color: var(--text-primary);
464
+ font-size: 13px;
465
+ transition: var(--transition);
466
+ font-family: inherit;
467
+ }
468
+
469
+ .form-input:focus, .form-select:focus, .form-textarea:focus {
470
+ outline: none;
471
+ border-color: var(--input-focus);
472
+ box-shadow: 0 0 0 3px rgba(14, 165, 233, 0.1);
473
+ }
474
+
475
+ .form-textarea {
476
+ resize: vertical;
477
+ min-height: 100px;
478
+ }
479
+
480
+ .button {
481
+ padding: 10px 16px;
482
+ border: none;
483
+ border-radius: 6px;
484
+ font-weight: 600;
485
+ font-size: 13px;
486
+ cursor: pointer;
487
+ transition: var(--transition);
488
+ text-transform: uppercase;
489
+ letter-spacing: 0.5px;
490
+ }
491
+
492
+ .button-primary {
493
+ background: var(--accent);
494
+ color: white;
495
+ }
496
+
497
+ .button-primary:hover {
498
+ background: var(--accent-hover);
499
+ }
500
+
501
+ .button-secondary {
502
+ background: var(--bg-secondary);
503
+ color: var(--text-primary);
504
+ border: 1px solid var(--border);
505
+ }
506
+
507
+ .button-secondary:hover {
508
+ background: var(--bg-tertiary);
509
+ }
510
+
511
+ .button-danger {
512
+ background: var(--danger);
513
+ color: white;
514
+ }
515
+
516
+ .button-danger:hover {
517
+ opacity: 0.9;
518
+ }
519
+
520
+ .button:disabled {
521
+ opacity: 0.5;
522
+ cursor: not-allowed;
523
+ }
524
+
525
+ .table {
526
+ width: 100%;
527
+ border-collapse: collapse;
528
+ font-size: 13px;
529
+ margin-bottom: 20px;
530
+ }
531
+
532
+ .table thead {
533
+ background: var(--bg-secondary);
534
+ border-bottom: 2px solid var(--border);
535
+ }
536
+
537
+ .table th {
538
+ padding: 12px;
539
+ text-align: left;
540
+ font-weight: 600;
541
+ color: var(--text-secondary);
542
+ text-transform: uppercase;
543
+ font-size: 11px;
544
+ letter-spacing: 0.5px;
545
+ }
546
+
547
+ .table td {
548
+ padding: 12px;
549
+ border-bottom: 1px solid var(--border);
550
+ }
551
+
552
+ .table tr:hover {
553
+ background: var(--bg-secondary);
554
+ }
555
+
556
+ .table tr.clickable {
557
+ cursor: pointer;
558
+ }
559
+
560
+ .truncate {
561
+ overflow: hidden;
562
+ text-overflow: ellipsis;
563
+ white-space: nowrap;
564
+ max-width: 400px;
565
+ }
566
+
567
+ .badge {
568
+ display: inline-block;
569
+ padding: 4px 8px;
570
+ border-radius: 4px;
571
+ font-size: 11px;
572
+ font-weight: 600;
573
+ text-transform: uppercase;
574
+ letter-spacing: 0.5px;
575
+ }
576
+
577
+ .badge-info { background: var(--bg-tertiary); color: var(--accent); }
578
+ .badge-success { background: #d1fae5; color: var(--success); }
579
+ .badge-warning { background: #fef3c7; color: var(--warning); }
580
+ .badge-danger { background: #fee2e2; color: var(--danger); }
581
+
582
+ .alert {
583
+ padding: 12px 16px;
584
+ border-radius: 6px;
585
+ margin-bottom: 15px;
586
+ font-size: 13px;
587
+ border-left: 4px solid;
588
+ }
589
+
590
+ .alert-info {
591
+ background: #cffafe;
592
+ border-left-color: var(--accent);
593
+ color: var(--accent);
594
+ }
595
+
596
+ .alert-success {
597
+ background: #d1fae5;
598
+ border-left-color: var(--success);
599
+ color: var(--success);
600
+ }
601
+
602
+ .alert-warning {
603
+ background: #fef3c7;
604
+ border-left-color: var(--warning);
605
+ color: var(--warning);
606
+ }
607
+
608
+ .alert-danger {
609
+ background: #fee2e2;
610
+ border-left-color: var(--danger);
611
+ color: var(--danger);
612
+ }
613
+
614
+ .hidden { display: none; }
615
+ .text-center { text-align: center; }
616
+ .text-muted { color: var(--text-tertiary); }
617
+ .mt-20 { margin-top: 20px; }
618
+ .mb-20 { margin-bottom: 20px; }
619
+ .gap-10 { gap: 10px; }
620
+
621
+ .loading {
622
+ text-align: center;
623
+ padding: 30px;
624
+ color: var(--text-tertiary);
625
+ }
626
+
627
+ .spinner {
628
+ border: 3px solid var(--bg-secondary);
629
+ border-top: 3px solid var(--accent);
630
+ border-radius: 50%;
631
+ width: 30px;
632
+ height: 30px;
633
+ animation: spin 1s linear infinite;
634
+ margin: 0 auto 10px;
635
+ }
636
+
637
+ @keyframes spin {
638
+ 0% { transform: rotate(0deg); }
639
+ 100% { transform: rotate(360deg); }
640
+ }
641
+
642
+ @media (max-width: 768px) {
643
+ .drawer-panel {
644
+ inset: 20px;
645
+ border-radius: 8px;
646
+ }
647
+
648
+ .drawer-header {
649
+ padding: 16px 16px 0;
650
+ }
651
+
652
+ .drawer-tabs {
653
+ padding: 12px 16px 0;
654
+ }
655
+
656
+ .drawer-body {
657
+ padding: 16px;
658
+ }
659
+
660
+ .drawer-tab {
661
+ padding: 10px 12px;
662
+ font-size: 12px;
663
+ }
664
+ }
665
+
666
+ @media (max-width: 480px) {
667
+ .drawer-panel {
668
+ inset: 0;
669
+ border-radius: 0;
670
+ }
671
+
672
+ .drawer {
673
+ backdrop-filter: none;
674
+ }
675
+
676
+ .drawer-backdrop {
677
+ display: none;
678
+ }
679
+ }
680
+
681
+ .button-group {
682
+ display: flex;
683
+ gap: 10px;
684
+ margin-bottom: 20px;
685
+ }
686
+
687
+ .button-group .button {
688
+ flex: 1;
689
+ }
690
+
691
+ .details-grid {
692
+ display: grid;
693
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
694
+ gap: 20px;
695
+ margin-bottom: 20px;
696
+ }
697
+
698
+ .detail-item {
699
+ background: rgba(255, 255, 255, 0.92);
700
+ border: 1px solid rgba(148, 163, 184, 0.18);
701
+ padding: 18px;
702
+ border-radius: 12px;
703
+ min-height: 86px;
704
+ display: flex;
705
+ flex-direction: column;
706
+ justify-content: space-between;
707
+ }
708
+
709
+ .detail-label {
710
+ font-size: 11px;
711
+ font-weight: 600;
712
+ color: var(--text-secondary);
713
+ text-transform: uppercase;
714
+ letter-spacing: 0.5px;
715
+ margin-bottom: 10px;
716
+ }
717
+
718
+ .detail-value {
719
+ font-size: 16px;
720
+ color: var(--text-primary);
721
+ font-weight: 700;
722
+ word-break: break-word;
723
+ }
724
+
725
+ .metadata-grid,
726
+ .metric-grid {
727
+ list-style: none;
728
+ padding: 0;
729
+ margin: 0;
730
+ display: grid;
731
+ grid-template-columns: repeat(3, minmax(220px, 1fr));
732
+ gap: 0;
733
+ border: 1px solid var(--border);
734
+ border-radius: 12px;
735
+ overflow: hidden;
736
+ background: var(--bg-secondary);
737
+ }
738
+
739
+ .metadata-item,
740
+ .metric-item {
741
+ display: flex;
742
+ flex-direction: column;
743
+ justify-content: center;
744
+ padding: 14px 18px;
745
+ border-bottom: 1px solid var(--border);
746
+ border-right: 1px solid var(--border);
747
+ background: transparent;
748
+ min-width: 0;
749
+ }
750
+
751
+ .metadata-item:nth-child(3n),
752
+ .metric-item:nth-child(3n) {
753
+ border-right: none;
754
+ }
755
+
756
+ .metadata-item:nth-last-child(-n+3),
757
+ .metric-item:nth-last-child(-n+3) {
758
+ border-bottom: none;
759
+ }
760
+
761
+ .metadata-item span,
762
+ .metric-item span {
763
+ text-transform: uppercase;
764
+ letter-spacing: 0.5px;
765
+ color: var(--text-secondary);
766
+ font-size: 11px;
767
+ margin-bottom: 8px;
768
+ }
769
+
770
+ .metadata-item strong,
771
+ .metric-item strong {
772
+ color: var(--text-primary);
773
+ font-size: 15px;
774
+ font-weight: 700;
775
+ text-align: right;
776
+ word-break: break-word;
777
+ max-width: 100%;
778
+ }
779
+
780
+ .session-panel {
781
+ background: var(--bg-secondary);
782
+ border: 1px solid var(--border);
783
+ border-radius: 12px;
784
+ overflow: hidden;
785
+ }
786
+
787
+ .session-panel .data-row {
788
+ display: flex;
789
+ justify-content: space-between;
790
+ align-items: center;
791
+ padding: 12px 18px;
792
+ border-bottom: 1px solid var(--border);
793
+ }
794
+
795
+ .session-panel .data-row:last-child {
796
+ border-bottom: none;
797
+ }
798
+
799
+ .session-block {
800
+ background: var(--bg-secondary);
801
+ border: 1px solid var(--border);
802
+ border-radius: 10px;
803
+ padding: 16px;
804
+ line-height: 1.7;
805
+ color: var(--text-primary);
806
+ white-space: pre-wrap;
807
+ }
808
+
809
+ .session-panel .data-row:last-child {
810
+ border-bottom: none;
811
+ }
812
+
813
+ .session-panel .data-label {
814
+ color: var(--text-secondary);
815
+ font-size: 11px;
816
+ font-weight: 600;
817
+ text-transform: uppercase;
818
+ letter-spacing: 0.5px;
819
+ }
820
+
821
+ .session-panel .data-value {
822
+ color: var(--text-primary);
823
+ font-size: 14px;
824
+ font-weight: 700;
825
+ text-align: right;
826
+ min-width: 100px;
827
+ }
828
+
829
+ .chat-thread {
830
+ display: flex;
831
+ flex-direction: column;
832
+ gap: 16px;
833
+ }
834
+
835
+ .chat-message {
836
+ border: 1px solid var(--border);
837
+ border-radius: 12px;
838
+ overflow: hidden;
839
+ background: var(--bg-secondary);
840
+ }
841
+
842
+ .chat-message-header {
843
+ display: flex;
844
+ justify-content: space-between;
845
+ align-items: center;
846
+ gap: 16px;
847
+ padding: 14px 16px;
848
+ background: var(--bg-primary);
849
+ border-bottom: 1px solid var(--border);
850
+ }
851
+
852
+ .chat-role {
853
+ font-size: 13px;
854
+ font-weight: 700;
855
+ color: var(--text-primary);
856
+ }
857
+
858
+ .chat-meta {
859
+ color: var(--text-secondary);
860
+ font-size: 12px;
861
+ }
862
+
863
+ .chat-message-block {
864
+ padding: 14px 16px;
865
+ line-height: 1.65;
866
+ color: var(--text-primary);
867
+ }
868
+
869
+ .chat-message-block.user {
870
+ background: var(--bg-primary);
871
+ }
872
+
873
+ .chat-message-block.assistant {
874
+ background: var(--bg-secondary);
875
+ }
876
+
877
+ .chat-message-label {
878
+ font-weight: 700;
879
+ margin-bottom: 10px;
880
+ }
881
+
882
+ .alert-text {
883
+ color: var(--text-secondary);
884
+ margin-top: 8px;
885
+ font-size: 12px;
886
+ line-height: 1.5;
887
+ }
888
+
889
+ @media (max-width: 1120px) {
890
+ .metadata-grid,
891
+ .metric-grid {
892
+ grid-template-columns: repeat(2, minmax(220px, 1fr));
893
+ }
894
+ }
895
+
896
+ @media (max-width: 768px) {
897
+ .metadata-grid,
898
+ .metric-grid {
899
+ grid-template-columns: 1fr;
900
+ }
901
+ }
902
+
903
+ .code-block {
904
+ background: var(--bg-secondary);
905
+ padding: 15px;
906
+ border-radius: 6px;
907
+ overflow-x: auto;
908
+ font-family: "Monaco", "Menlo", monospace;
909
+ font-size: 12px;
910
+ color: var(--text-primary);
911
+ margin-bottom: 20px;
912
+ }
913
+
914
+ .status-indicator {
915
+ display: inline-block;
916
+ width: 8px;
917
+ height: 8px;
918
+ border-radius: 50%;
919
+ margin-right: 6px;
920
+ }
921
+
922
+ .status-online { background: var(--success); }
923
+ .status-offline { background: var(--danger); }
924
+ .status-idle { background: var(--warning); }
925
+ """
926
+
927
+ # ─────────────────────────────────────────────
928
+ # Database Helpers
929
+ # ─────────────────────────────────────────────
930
+
931
+
932
+ def get_db():
933
+ """Get database connection with proper settings."""
934
+ conn = sqlite3.connect(str(DB_PATH), timeout=10)
935
+ conn.row_factory = sqlite3.Row
936
+ conn.execute("PRAGMA journal_mode=WAL")
937
+ conn.execute("PRAGMA synchronous=NORMAL")
938
+ return conn
939
+
940
+
941
+ def query_rows(sql, params=()):
942
+ """Execute SELECT and return rows as dicts."""
943
+ try:
944
+ conn = get_db()
945
+ cursor = conn.execute(sql, params)
946
+ rows = [dict(row) for row in cursor.fetchall()]
947
+ conn.close()
948
+ return rows
949
+ except Exception as e:
950
+ return {"error": str(e)}
951
+
952
+
953
+ def query_one(sql, params=()):
954
+ """Execute SELECT and return single row or None."""
955
+ rows = query_rows(sql, params)
956
+ if isinstance(rows, dict) and "error" in rows:
957
+ return rows
958
+ return rows[0] if rows else None
959
+
960
+
961
+ def safe_rows(rows):
962
+ """Normalize query_rows output to an array for APIs."""
963
+ if isinstance(rows, dict) and "error" in rows:
964
+ return []
965
+ return rows or []
966
+
967
+
968
+ def safe_one(row):
969
+ """Normalize query_one output to a dict or None."""
970
+ if isinstance(row, dict) and "error" in row:
971
+ return None
972
+ return row
973
+
974
+
975
+ def table_exists(table_name):
976
+ """Return True if a table exists in the current database."""
977
+ try:
978
+ row = query_one("SELECT name FROM sqlite_master WHERE type='table' AND name=?", (table_name,))
979
+ return bool(row)
980
+ except Exception:
981
+ return False
982
+
983
+
984
+ def execute_stmt(sql, params=()):
985
+ """Execute INSERT/UPDATE/DELETE statement."""
986
+ try:
987
+ conn = get_db()
988
+ conn.execute(sql, params)
989
+ conn.commit()
990
+ conn.close()
991
+ return {"ok": True}
992
+ except Exception as e:
993
+ return {"error": str(e)}
994
+
995
+ # ─────────────────────────────────────────────
996
+ # Data Retrieval Functions
997
+ # ─────────────────────────────────────────────
998
+
999
+
1000
+ def get_overview():
1001
+ """Dashboard overview stats."""
1002
+ try:
1003
+ has_analysis = table_exists('session_analysis')
1004
+ has_exchanges = table_exists('exchanges')
1005
+ has_signals = table_exists('exchange_signals')
1006
+ has_alerts = table_exists('anomaly_alerts')
1007
+
1008
+ stats = query_one(f"""
1009
+ SELECT
1010
+ (SELECT COUNT(*) FROM sessions) as total_sessions,
1011
+ {("(SELECT COUNT(*) FROM exchanges)" if has_exchanges else "0")} as total_exchanges,
1012
+ {("(SELECT AVG(heat_score) FROM session_analysis WHERE heat_score IS NOT NULL)" if has_analysis else "0")} as avg_heat,
1013
+ {("(SELECT COUNT(*) FROM session_analysis WHERE heat_score >= 50)" if has_analysis else "0")} as critical_sessions,
1014
+ {("(SELECT COUNT(*) FROM anomaly_alerts)" if has_alerts else "0")} as alert_count,
1015
+ (SELECT COUNT(DISTINCT workspace_id) FROM sessions) as workspace_count,
1016
+ (SELECT MAX(created_at) FROM sessions) as last_session
1017
+ """)
1018
+
1019
+ heat_dist = safe_rows(query_rows("""
1020
+ SELECT
1021
+ CASE
1022
+ WHEN heat_score < 20 THEN '0-19'
1023
+ WHEN heat_score < 40 THEN '20-39'
1024
+ WHEN heat_score < 60 THEN '40-59'
1025
+ WHEN heat_score < 80 THEN '60-79'
1026
+ ELSE '80-100'
1027
+ END as range,
1028
+ COUNT(*) as count
1029
+ FROM session_analysis
1030
+ WHERE heat_score IS NOT NULL
1031
+ GROUP BY range
1032
+ ORDER BY range
1033
+ """)) if has_analysis else []
1034
+
1035
+ keywords = safe_rows(query_rows("""
1036
+ SELECT matched_keyword as keyword, SUM(count) as total_count
1037
+ FROM (
1038
+ SELECT matched_keyword, COUNT(*) as count
1039
+ FROM exchange_signals
1040
+ WHERE matched_keyword IS NOT NULL
1041
+ GROUP BY matched_keyword
1042
+ )
1043
+ GROUP BY matched_keyword
1044
+ ORDER BY total_count DESC
1045
+ LIMIT 15
1046
+ """)) if has_signals else []
1047
+
1048
+ if has_analysis:
1049
+ recent = safe_rows(query_rows("""
1050
+ SELECT s.session_id as id, s.title, sa.heat_score,
1051
+ {("(SELECT COUNT(*) FROM exchanges WHERE exchanges.session_id = s.session_id)" if has_exchanges else "0")} as exchange_count,
1052
+ s.created_at
1053
+ FROM sessions s
1054
+ LEFT JOIN session_analysis sa ON sa.session_id = s.session_id
1055
+ ORDER BY s.created_at DESC
1056
+ LIMIT 10
1057
+ """))
1058
+ else:
1059
+ recent = safe_rows(query_rows("""
1060
+ SELECT s.session_id as id, s.title, NULL as heat_score,
1061
+ {("(SELECT COUNT(*) FROM exchanges WHERE exchanges.session_id = s.session_id)" if has_exchanges else "0")} as exchange_count,
1062
+ s.created_at
1063
+ FROM sessions s
1064
+ ORDER BY s.created_at DESC
1065
+ LIMIT 10
1066
+ """))
1067
+
1068
+ stats = safe_one(stats)
1069
+ return {
1070
+ "stats": dict(stats) if stats else {},
1071
+ "heat_distribution": heat_dist,
1072
+ "keywords": keywords,
1073
+ "recent_sessions": recent
1074
+ }
1075
+ except Exception as e:
1076
+ return {"error": str(e)}
1077
+
1078
+
1079
+ def get_sessions(limit=50, offset=0):
1080
+ """List all sessions with heat scores."""
1081
+ try:
1082
+ has_analysis = table_exists('session_analysis')
1083
+ has_exchanges = table_exists('exchanges')
1084
+ exchange_count_expr = "(SELECT COUNT(*) FROM exchanges WHERE exchanges.session_id = s.session_id)" if has_exchanges else "0"
1085
+
1086
+ if has_analysis:
1087
+ sessions = safe_rows(query_rows(f"""
1088
+ SELECT s.session_id as id, s.title, sa.heat_score, s.workspace_id,
1089
+ {exchange_count_expr} as exchange_count,
1090
+ s.created_at
1091
+ FROM sessions s
1092
+ LEFT JOIN session_analysis sa ON sa.session_id = s.session_id
1093
+ ORDER BY s.created_at DESC
1094
+ LIMIT ? OFFSET ?
1095
+ """, (limit, offset)))
1096
+ else:
1097
+ sessions = safe_rows(query_rows(f"""
1098
+ SELECT s.session_id as id, s.title, NULL as heat_score, s.workspace_id,
1099
+ {exchange_count_expr} as exchange_count,
1100
+ s.created_at
1101
+ FROM sessions s
1102
+ ORDER BY s.created_at DESC
1103
+ LIMIT ? OFFSET ?
1104
+ """, (limit, offset)))
1105
+
1106
+ total = safe_one(query_one("SELECT COUNT(*) as count FROM sessions"))
1107
+
1108
+ return {
1109
+ "sessions": sessions,
1110
+ "total": total["count"] if total else 0,
1111
+ "limit": limit,
1112
+ "offset": offset
1113
+ }
1114
+ except Exception as e:
1115
+ return {"error": str(e)}
1116
+
1117
+
1118
+ def get_session_detail(session_id):
1119
+ """Get full session with all exchanges and signals."""
1120
+ if not session_id:
1121
+ return {"error": "Missing session_id"}
1122
+
1123
+ try:
1124
+ has_exchanges = table_exists('exchanges')
1125
+ has_tool_calls = table_exists('tool_calls')
1126
+ has_vfs = table_exists('vfs')
1127
+ has_alerts = table_exists('anomaly_alerts')
1128
+ has_signals = table_exists('exchange_signals')
1129
+ has_analysis = table_exists('session_analysis')
1130
+
1131
+ session = safe_one(query_one("SELECT * FROM sessions WHERE session_id = ?", (session_id,)))
1132
+ if not session:
1133
+ return {"error": "Session not found"}
1134
+
1135
+ exchanges = safe_rows(query_rows("""
1136
+ SELECT id, exchange_index, user_message as user_input, response_text as assistant_response,
1137
+ tool_calls, tool_call_count, ingested_at as created_at
1138
+ FROM exchanges
1139
+ WHERE session_id = ?
1140
+ ORDER BY ingested_at ASC
1141
+ """, (session_id,))) if has_exchanges else []
1142
+
1143
+ tool_calls = safe_rows(query_rows("""
1144
+ SELECT id, session_id, exchange_index, request_id, tool_call_id, tool_name,
1145
+ file_path, arguments_json, has_output, ingested_at
1146
+ FROM tool_calls
1147
+ WHERE session_id = ?
1148
+ ORDER BY ingested_at ASC
1149
+ """, (session_id,))) if has_tool_calls else []
1150
+
1151
+ vfs_entries = safe_rows(query_rows("""
1152
+ SELECT id, source_type, source_path, filename, content_type, size_bytes, sha256, ingested_at
1153
+ FROM vfs
1154
+ WHERE session_id = ?
1155
+ ORDER BY filename ASC
1156
+ """, (session_id,))) if has_vfs else []
1157
+
1158
+ alerts = safe_rows(query_rows("""
1159
+ SELECT id, alert_type, severity, message, created_at
1160
+ FROM anomaly_alerts
1161
+ WHERE session_id = ?
1162
+ ORDER BY created_at DESC
1163
+ """, (session_id,))) if has_alerts else []
1164
+
1165
+ signals = safe_rows(query_rows("""
1166
+ SELECT * FROM exchange_signals
1167
+ WHERE session_id = ?
1168
+ ORDER BY created_at DESC
1169
+ """, (session_id,))) if has_signals else []
1170
+
1171
+ signal_summary = safe_rows(query_rows("""
1172
+ SELECT signal_type, COUNT(*) as count
1173
+ FROM exchange_signals
1174
+ WHERE session_id = ?
1175
+ GROUP BY signal_type
1176
+ """, (session_id,))) if has_signals else []
1177
+
1178
+ analysis = safe_one(query_one("""
1179
+ SELECT * FROM session_analysis
1180
+ WHERE session_id = ?
1181
+ LIMIT 1
1182
+ """, (session_id,))) if has_analysis else None
1183
+
1184
+ return {
1185
+ "session": dict(session),
1186
+ "analysis": analysis,
1187
+ "exchanges": exchanges,
1188
+ "tool_calls": tool_calls,
1189
+ "vfs": vfs_entries,
1190
+ "alerts": alerts,
1191
+ "signals": signals,
1192
+ "signal_summary": signal_summary
1193
+ }
1194
+ except Exception as e:
1195
+ return {"error": str(e)}
1196
+
1197
+
1198
+ def get_search_results(query, limit=50):
1199
+ """Full-text search across exchanges."""
1200
+ try:
1201
+ results = query_rows("""
1202
+ SELECT DISTINCT
1203
+ s.id as session_id,
1204
+ s.title,
1205
+ s.heat_score,
1206
+ e.id as exchange_id,
1207
+ e.user_input,
1208
+ e.assistant_response,
1209
+ RANK() OVER (ORDER BY rank) as relevance
1210
+ FROM sessions s
1211
+ JOIN exchanges e ON s.id = e.session_id
1212
+ JOIN full_text_search fts ON e.id = fts.exchange_id
1213
+ WHERE fts.full_text_search MATCH ?
1214
+ ORDER BY rank
1215
+ LIMIT ?
1216
+ """, (query, limit))
1217
+ return {"results": results, "query": query, "count": len(results)}
1218
+ except Exception as e:
1219
+ return {"error": str(e)}
1220
+
1221
+
1222
+ def get_workspaces():
1223
+ """List all workspaces with session counts."""
1224
+ try:
1225
+ workspaces = query_rows("""
1226
+ SELECT DISTINCT workspace_id,
1227
+ COUNT(*) as session_count,
1228
+ MAX(created_at) as last_session
1229
+ FROM sessions
1230
+ WHERE workspace_id IS NOT NULL
1231
+ GROUP BY workspace_id
1232
+ ORDER BY session_count DESC
1233
+ """)
1234
+ return {"workspaces": workspaces}
1235
+ except Exception as e:
1236
+ return {"error": str(e)}
1237
+
1238
+
1239
+ def get_workspace_detail(workspace_id):
1240
+ """Get all sessions for a workspace."""
1241
+ try:
1242
+ sessions = query_rows("""
1243
+ SELECT s.session_id as id, s.title, sa.heat_score,
1244
+ (SELECT COUNT(*) FROM exchanges WHERE exchanges.session_id = s.session_id) as exchange_count,
1245
+ s.created_at
1246
+ FROM sessions s
1247
+ LEFT JOIN session_analysis sa ON sa.session_id = s.session_id
1248
+ WHERE s.workspace_id = ?
1249
+ ORDER BY s.created_at DESC
1250
+ """, (workspace_id,))
1251
+ return {"workspace_id": workspace_id, "sessions": sessions}
1252
+ except Exception as e:
1253
+ return {"error": str(e)}
1254
+
1255
+
1256
+ def get_memory():
1257
+ """Get all memory files."""
1258
+ try:
1259
+ memory = query_rows("""
1260
+ SELECT id, name, size, created_at, updated_at
1261
+ FROM memory_files
1262
+ ORDER BY updated_at DESC
1263
+ """)
1264
+ return {"memory": memory}
1265
+ except Exception as e:
1266
+ return {"error": str(e)}
1267
+
1268
+
1269
+ def get_tool_calls(query_str=None, limit=50):
1270
+ """Search tool calls."""
1271
+ try:
1272
+ if query_str:
1273
+ results = query_rows("""
1274
+ SELECT tc.*, e.session_id, s.title as session_title
1275
+ FROM tool_calls tc
1276
+ JOIN exchanges e ON tc.exchange_id = e.id
1277
+ JOIN sessions s ON e.session_id = s.id
1278
+ WHERE tc.tool_name LIKE ? OR tc.arguments LIKE ?
1279
+ ORDER BY tc.created_at DESC
1280
+ LIMIT ?
1281
+ """, (f"%{query_str}%", f"%{query_str}%", limit))
1282
+ else:
1283
+ results = query_rows("""
1284
+ SELECT tc.*, e.session_id, s.title as session_title
1285
+ FROM tool_calls tc
1286
+ JOIN exchanges e ON tc.exchange_id = e.id
1287
+ JOIN sessions s ON e.session_id = s.id
1288
+ ORDER BY tc.created_at DESC
1289
+ LIMIT ?
1290
+ """, (limit,))
1291
+ return {"tool_calls": results, "query": query_str, "count": len(results)}
1292
+ except Exception as e:
1293
+ return {"error": str(e)}
1294
+
1295
+
1296
+ def get_vfs(session_id):
1297
+ """List VFS files for a session."""
1298
+ try:
1299
+ vfs = query_rows("""
1300
+ SELECT id, session_id, path, size, created_at
1301
+ FROM vfs
1302
+ WHERE session_id = ?
1303
+ ORDER BY path
1304
+ """, (session_id,))
1305
+ return {"vfs": vfs, "session_id": session_id}
1306
+ except Exception as e:
1307
+ return {"error": str(e)}
1308
+
1309
+
1310
+ def get_alerts(limit=50):
1311
+ """Get anomaly alerts."""
1312
+ try:
1313
+ alerts = query_rows("""
1314
+ SELECT id, session_id, alert_type, message, severity, created_at
1315
+ FROM anomaly_alerts
1316
+ ORDER BY created_at DESC
1317
+ LIMIT ?
1318
+ """, (limit,))
1319
+
1320
+ session_titles = {}
1321
+ for alert in alerts:
1322
+ if alert["session_id"] not in session_titles:
1323
+ sess = query_one("SELECT title FROM sessions WHERE session_id = ?", (alert["session_id"],))
1324
+ session_titles[alert["session_id"]] = sess["title"] if sess else "Unknown"
1325
+
1326
+ for alert in alerts:
1327
+ alert["session_title"] = session_titles.get(alert["session_id"], "Unknown")
1328
+
1329
+ return {"alerts": alerts}
1330
+ except Exception as e:
1331
+ return {"error": str(e)}
1332
+
1333
+
1334
+ def get_behavioral_signals(session_id=None):
1335
+ """Get behavioral signal analysis."""
1336
+ try:
1337
+ if session_id:
1338
+ signals = query_rows("""
1339
+ SELECT signal_type, COUNT(*) as count
1340
+ FROM exchange_signals
1341
+ WHERE session_id = ?
1342
+ GROUP BY signal_type
1343
+ """, (session_id,))
1344
+ else:
1345
+ signals = query_rows("""
1346
+ SELECT signal_type, COUNT(*) as count
1347
+ FROM exchange_signals
1348
+ GROUP BY signal_type
1349
+ """)
1350
+ return {"signals": signals}
1351
+ except Exception as e:
1352
+ return {"error": str(e)}
1353
+
1354
+
1355
+ def get_tokens(session_id=None):
1356
+ """Get token usage analysis."""
1357
+ try:
1358
+ if session_id:
1359
+ tokens = query_rows("""
1360
+ SELECT
1361
+ SUM(CAST(json_extract(metadata, '$.token_count') AS INTEGER)) as total_tokens,
1362
+ COUNT(*) as exchange_count
1363
+ FROM exchanges
1364
+ WHERE session_id = ?
1365
+ """, (session_id,))
1366
+ else:
1367
+ tokens = query_rows("""
1368
+ SELECT
1369
+ SUM(CAST(json_extract(metadata, '$.token_count') AS INTEGER)) as total_tokens,
1370
+ COUNT(*) as exchange_count
1371
+ FROM exchanges
1372
+ """)
1373
+ return {"tokens": tokens}
1374
+ except Exception as e:
1375
+ return {"error": str(e)}
1376
+
1377
+ # ─────────────────────────────────────────────
1378
+ # Action Execution (Background Threading)
1379
+ # ─────────────────────────────────────────────
1380
+
1381
+
1382
+ ACTION_STATE: Dict[str, Any] = {}
1383
+ ACTION_LOCK = threading.Lock()
1384
+
1385
+
1386
+ def run_action_background(action_id, action_name):
1387
+ """Execute pipeline action in background thread."""
1388
+ with ACTION_LOCK:
1389
+ ACTION_STATE[action_id] = {
1390
+ "status": "running",
1391
+ "action": action_name,
1392
+ "started_at": datetime.now().isoformat(),
1393
+ "output": ""
1394
+ }
1395
+
1396
+ try:
1397
+ if action_name == "sync":
1398
+ result = subprocess.run(
1399
+ ["python3", str(PACKAGE_DIR.parent / "pipeline" / "ingest.py")],
1400
+ capture_output=True,
1401
+ text=True,
1402
+ timeout=300
1403
+ )
1404
+ elif action_name == "reconstruct":
1405
+ result = subprocess.run(
1406
+ ["python3", str(PACKAGE_DIR.parent / "pipeline" / "reconstruct.py")],
1407
+ capture_output=True,
1408
+ text=True,
1409
+ timeout=300
1410
+ )
1411
+ elif action_name == "embed-build":
1412
+ result = subprocess.run(
1413
+ ["python3", str(PACKAGE_DIR.parent / "pipeline" / "embed.py"), "build"],
1414
+ capture_output=True,
1415
+ text=True,
1416
+ timeout=600
1417
+ )
1418
+ elif action_name == "watch-start":
1419
+ result = subprocess.run(
1420
+ ["python3", str(PACKAGE_DIR.parent / "pipeline" / "watcher.py"), "start"],
1421
+ capture_output=True,
1422
+ text=True,
1423
+ timeout=30
1424
+ )
1425
+ else:
1426
+ result = None
1427
+
1428
+ with ACTION_LOCK:
1429
+ if result:
1430
+ ACTION_STATE[action_id]["status"] = "completed" if result.returncode == 0 else "failed"
1431
+ ACTION_STATE[action_id]["output"] = result.stdout + result.stderr
1432
+ ACTION_STATE[action_id]["returncode"] = result.returncode
1433
+ ACTION_STATE[action_id]["completed_at"] = datetime.now().isoformat()
1434
+ except Exception as e:
1435
+ with ACTION_LOCK:
1436
+ ACTION_STATE[action_id]["status"] = "error"
1437
+ ACTION_STATE[action_id]["output"] = str(e)
1438
+ ACTION_STATE[action_id]["completed_at"] = datetime.now().isoformat()
1439
+
1440
+ # ─────────────────────────────────────────────
1441
+ # WSGI Application
1442
+ # ─────────────────────────────────────────────
1443
+
1444
+
1445
+ INDEX_HTML = """
1446
+ <!DOCTYPE html>
1447
+ <html>
1448
+ <head>
1449
+ <meta charset="utf-8">
1450
+ <meta name="viewport" content="width=device-width, initial-scale=1">
1451
+ <title>Code Data Ark</title>
1452
+ <style>{{STYLE_CSS}}</style>
1453
+ </head>
1454
+ <body>
1455
+ <div id="root">
1456
+ <div class="sidebar">
1457
+ <div class="sidebar-header">
1458
+ <div class="sidebar-title">Code Data Ark</div>
1459
+ <div style="font-size: 11px; color: var(--text-tertiary); margin-top: 5px;">
1460
+ Intelligence & Analysis
1461
+ </div>
1462
+ </div>
1463
+
1464
+ <div class="nav-group">
1465
+ <div class="nav-group-title">Core</div>
1466
+ <div class="nav-item active" data-page="dashboard"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="3" width="7" height="8" rx="1"/><rect x="14" y="3" width="7" height="5" rx="1"/><rect x="14" y="12" width="7" height="9" rx="1"/><rect x="3" y="14" width="7" height="6" rx="1"/></svg>Dashboard</div> # noqa: E501
1467
+ <div class="nav-item" data-page="sessions"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>Sessions</div> # noqa: E501
1468
+ <div class="nav-item" data-page="search"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>Search</div> # noqa: E501
1469
+ </div>
1470
+
1471
+ <div class="nav-group">
1472
+ <div class="nav-group-title">Analysis</div>
1473
+ <div class="nav-item" data-page="heat"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 14.5a4 4 0 0 1 8 0c0 2.2-2.5 5.5-4 7.5-1.5-2-4-5.3-4-7.5z"/><path d="M12 2.5c0 4.5-2 7.5-2 10.5a4 4 0 0 0 4 4c1.5 0 2-1 2-1s2 1 2-2c0-5-5-8-6-11.5z"/></svg>Heat Analysis</div> # noqa: E501
1474
+ <div class="nav-item" data-page="keywords"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M20.59 13.41 13.41 20.59a2 2 0 0 1-2.83 0L3.59 13.6a2 2 0 0 1 0-2.83L10.77 3.59a2 2 0 0 1 2.83 0l7.17 7.17a2 2 0 0 1 0 2.83z"/><path d="M7 7h.01"/></svg>Keywords</div> # noqa: E501
1475
+ <div class="nav-item" data-page="signals"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12.55a11 11 0 0 1 14.08 0"/><path d="M8.5 16.5a6 6 0 0 1 7 0"/><path d="M12 20a2 2 0 0 1 2-2 2 2 0 0 1 2 2"/></svg>Signals</div> # noqa: E501
1476
+ <div class="nav-item" data-page="behavior"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 22a4 4 0 0 0 4-4v-1.5a3.5 3.5 0 0 0-3.5-3.5H11.5A3.5 3.5 0 0 0 8 16.5V18a4 4 0 0 0 4 4z"/><path d="M8 2c-1.11 0-2 .9-2 2v3h12V4c0-1.1-.89-2-2-2H8z"/></svg>Behavior</div> # noqa: E501
1477
+ </div>
1478
+
1479
+ <div class="nav-group">
1480
+ <div class="nav-group-title">Navigation</div>
1481
+ <div class="nav-item" data-page="workspaces"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 5a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H6a2 2 0 0 1-2-2Z"/></svg>Workspaces</div> # noqa: E501
1482
+ <div class="nav-item" data-page="tools"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M14.7 10.3 13.4 11.6 14.1 12.3a2 2 0 0 1 0 2.83l-5.7 5.7a2 2 0 0 1-2.83 0L3.4 17.7a2 2 0 0 1 0-2.83l5.7-5.7a2 2 0 0 1 2.83 0l.7.7 1.3-1.3"/><path d="M9 14.6l-2-2"/></svg>Tool Calls</div> # noqa: E501
1483
+ <div class="nav-item" data-page="memory"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><ellipse cx="12" cy="5" rx="8" ry="3"/><path d="M4 5v6c0 1.66 3.58 3 8 3s8-1.34 8-3V5"/><path d="M4 11v6c0 1.66 3.58 3 8 3s8-1.34 8-3v-6"/></svg>Memory</div> # noqa: E501
1484
+ <div class="nav-item" data-page="tokens"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 12h10"/><path d="M8 8h8"/><path d="M8 16h8"/><path d="M12 4v16"/></svg>Tokens</div> # noqa: E501
1485
+ </div>
1486
+
1487
+ <div class="nav-group">
1488
+ <div class="nav-group-title">Intelligence</div>
1489
+ <div class="nav-item" data-page="alerts"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M10.29 3.86 1.82 18a2 2 0 0 0 1.71 3h16.94a2 2 0 0 0 1.71-3L13.71 3.86a2 2 0 0 0-3.42 0z"/><line x1="12" y1="9" x2="12" y2="13"/><line x1="12" y1="17" x2="12.01" y2="17"/></svg>Alerts</div> # noqa: E501
1490
+ <div class="nav-item" data-page="recommendations"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 18h6"/><path d="M10 22h4"/><path d="M9 9a3 3 0 0 1 6 0c0 1.38-.56 2.63-1.5 3.5A3 3 0 0 0 12 17a3 3 0 0 0-1.5-4.5C9.56 11.63 9 10.38 9 9z"/></svg>Recommendations</div> # noqa: E501
1491
+ <div class="nav-item" data-page="topics"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="M12 2 2 7l10 5 10-5-10-5z"/><path d="M2 17l10 5 10-5"/><path d="M2 12l10 5 10-5"/></svg>Topics</div> # noqa: E501
1492
+ </div>
1493
+
1494
+ <div class="nav-group">
1495
+ <div class="nav-group-title">System</div>
1496
+ <div class="nav-item" data-page="pipeline"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><circle cx="6" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M6 9v6h12"/></svg>Pipeline</div> # noqa: E501
1497
+ <div class="nav-item" data-page="query"><svg class="icon" viewBox="0 0 24 24" aria-hidden="true"><path d="m8 9 4-4 4 4"/><path d="m8 15 4-4 4 4"/></svg>Raw Query</div> # noqa: E501
1498
+ </div>
1499
+ </div>
1500
+
1501
+ <div class="content" id="main-content">
1502
+ <!-- Pages rendered here -->
1503
+ </div>
1504
+ </div>
1505
+ <div id="detail-drawer" class="drawer">
1506
+ <div class="drawer-backdrop" onclick="closeSessionDrawer()"></div>
1507
+ <div class="drawer-panel">
1508
+ <div class="drawer-header">
1509
+ <div class="drawer-title">
1510
+ <div class="title" id="drawer-session-title">Session Details</div>
1511
+ <div class="subtitle" id="drawer-session-subtitle">Full chat history and session metadata.</div>
1512
+ </div>
1513
+ <button class="drawer-close" onclick="closeSessionDrawer()" aria-label="Close session details">×</button>
1514
+ </div>
1515
+ <div class="drawer-tabs" id="drawer-tabs">
1516
+ <div class="drawer-tab active" data-tab="overview" onclick="switchDrawerTab('overview')">Overview</div>
1517
+ <div class="drawer-tab" data-tab="analysis" onclick="switchDrawerTab('analysis')">Analysis</div>
1518
+ <div class="drawer-tab" data-tab="chat" onclick="switchDrawerTab('chat')">Chat</div>
1519
+ <div class="drawer-tab" data-tab="tools" onclick="switchDrawerTab('tools')">Tool Calls</div>
1520
+ <div class="drawer-tab" data-tab="signals" onclick="switchDrawerTab('signals')">Signals</div>
1521
+ <div class="drawer-tab" data-tab="files" onclick="switchDrawerTab('files')">Files</div>
1522
+ <div class="drawer-tab" data-tab="alerts" onclick="switchDrawerTab('alerts')">Alerts</div>
1523
+ <div class="drawer-tab" data-tab="raw" onclick="switchDrawerTab('raw')">Raw</div>
1524
+ </div>
1525
+ <div class="drawer-body" id="drawer-body">
1526
+ <div class="spinner"></div>
1527
+ Loading session details...
1528
+ </div>
1529
+ </div>
1530
+ </div>
1531
+
1532
+ <script>{{APP_JS}}</script>
1533
+ </body>
1534
+ </html>
1535
+ """
1536
+
1537
+
1538
+ def render_page(page_name):
1539
+ """Render page based on name."""
1540
+ if page_name == "dashboard":
1541
+ return render_dashboard()
1542
+ elif page_name == "sessions":
1543
+ return render_sessions()
1544
+ elif page_name == "search":
1545
+ return render_search()
1546
+ elif page_name == "heat":
1547
+ return render_heat()
1548
+ elif page_name == "keywords":
1549
+ return render_keywords()
1550
+ elif page_name == "signals":
1551
+ return render_signals()
1552
+ elif page_name == "behavior":
1553
+ return render_behavior()
1554
+ elif page_name == "workspaces":
1555
+ return render_workspaces()
1556
+ elif page_name == "tools":
1557
+ return render_tools()
1558
+ elif page_name == "memory":
1559
+ return render_memory()
1560
+ elif page_name == "tokens":
1561
+ return render_tokens()
1562
+ elif page_name == "alerts":
1563
+ return render_alerts()
1564
+ elif page_name == "recommendations":
1565
+ return render_recommendations()
1566
+ elif page_name == "topics":
1567
+ return render_topics()
1568
+ elif page_name == "pipeline":
1569
+ return render_pipeline()
1570
+ elif page_name == "query":
1571
+ return render_query()
1572
+ else:
1573
+ return render_dashboard()
1574
+
1575
+
1576
+ def render_dashboard():
1577
+ """Dashboard page."""
1578
+ return """
1579
+ <div class="page-header">
1580
+ <div class="page-title">Dashboard</div>
1581
+ <div class="page-subtitle">Behavioral intelligence summary, heat distribution, and pipeline status.</div>
1582
+ </div>
1583
+ <div id="dashboard-content" class="loading">
1584
+ <div class="spinner"></div>
1585
+ Loading overview...
1586
+ </div>
1587
+ """
1588
+
1589
+
1590
+ def render_sessions():
1591
+ """Sessions list page."""
1592
+ return """
1593
+ <div class="page-header">
1594
+ <div class="page-title">Sessions</div>
1595
+ <div class="page-subtitle">Browse all recorded sessions with heat scores and metrics.</div>
1596
+ </div>
1597
+ <div id="sessions-content" class="loading">
1598
+ <div class="spinner"></div>
1599
+ Loading sessions...
1600
+ </div>
1601
+ """
1602
+
1603
+
1604
+ def render_search():
1605
+ """Full-text search page."""
1606
+ return """
1607
+ <div class="page-header">
1608
+ <div class="page-title">Search</div>
1609
+ <div class="page-subtitle">Full-text search across all exchanges and content.</div>
1610
+ </div>
1611
+ <div class="card mb-20">
1612
+ <div class="form-group">
1613
+ <label class="form-label">Search Query</label>
1614
+ <input type="text" id="search-input" class="form-input" placeholder="Enter search terms...">
1615
+ </div>
1616
+ <button class="button button-primary" onclick="performSearch()">Search</button>
1617
+ </div>
1618
+ <div id="search-results" class="loading" style="display: none;">
1619
+ <div class="spinner"></div>
1620
+ Searching...
1621
+ </div>
1622
+ """
1623
+
1624
+
1625
+ def render_heat():
1626
+ """Heat analysis page."""
1627
+ return """
1628
+ <div class="page-header">
1629
+ <div class="page-title">Heat Analysis</div>
1630
+ <div class="page-subtitle">Frustration and behavioral heat patterns.</div>
1631
+ </div>
1632
+ <div id="heat-content" class="loading">
1633
+ <div class="spinner"></div>
1634
+ Loading heat analysis...
1635
+ </div>
1636
+ """
1637
+
1638
+
1639
+ def render_keywords():
1640
+ """Keywords page."""
1641
+ return """
1642
+ <div class="page-header">
1643
+ <div class="page-title">Keywords</div>
1644
+ <div class="page-subtitle">Most common behavioral signal keywords.</div>
1645
+ </div>
1646
+ <div id="keywords-content" class="loading">
1647
+ <div class="spinner"></div>
1648
+ Loading keywords...
1649
+ </div>
1650
+ """
1651
+
1652
+
1653
+ def render_signals():
1654
+ """Behavioral signals page."""
1655
+ return """
1656
+ <div class="page-header">
1657
+ <div class="page-title">Behavioral Signals</div>
1658
+ <div class="page-subtitle">Correction, frustration, redirects, and approval patterns.</div>
1659
+ </div>
1660
+ <div class="card">
1661
+ <p>Behavioral signal analysis coming soon.</p>
1662
+ </div>
1663
+ """
1664
+
1665
+
1666
+ def render_behavior():
1667
+ """Behavior intelligence page."""
1668
+ return """
1669
+ <div class="page-header">
1670
+ <div class="page-title">Behavior Intelligence</div>
1671
+ <div class="page-subtitle">Aggregate behavioral patterns and trends.</div>
1672
+ </div>
1673
+ <div class="card">
1674
+ <p>Behavior intelligence analysis coming soon.</p>
1675
+ </div>
1676
+ """
1677
+
1678
+
1679
+ def render_workspaces():
1680
+ """Workspaces page."""
1681
+ return """
1682
+ <div class="page-header">
1683
+ <div class="page-title">Workspaces</div>
1684
+ <div class="page-subtitle">Browse sessions by workspace.</div>
1685
+ </div>
1686
+ <div id="workspaces-content" class="loading">
1687
+ <div class="spinner"></div>
1688
+ Loading workspaces...
1689
+ </div>
1690
+ """
1691
+
1692
+
1693
+ def render_tools():
1694
+ """Tool calls page."""
1695
+ return """
1696
+ <div class="page-header">
1697
+ <div class="page-title">Tool Calls</div>
1698
+ <div class="page-subtitle">Search and analyze tool invocations.</div>
1699
+ </div>
1700
+ <div class="card mb-20">
1701
+ <div class="form-group">
1702
+ <label class="form-label">Search Tools</label>
1703
+ <input type="text" id="tool-search" class="form-input" placeholder="Enter tool name or pattern...">
1704
+ </div>
1705
+ <button class="button button-primary" onclick="searchTools()">Search</button>
1706
+ </div>
1707
+ <div id="tools-content" class="loading" style="display: none;">
1708
+ <div class="spinner"></div>
1709
+ Searching...
1710
+ </div>
1711
+ """
1712
+
1713
+
1714
+ def render_memory():
1715
+ """Memory files page."""
1716
+ return """
1717
+ <div class="page-header">
1718
+ <div class="page-title">Memory</div>
1719
+ <div class="page-subtitle">Stored memory files and knowledge base.</div>
1720
+ </div>
1721
+ <div id="memory-content" class="loading">
1722
+ <div class="spinner"></div>
1723
+ Loading memory...
1724
+ </div>
1725
+ """
1726
+
1727
+
1728
+ def render_tokens():
1729
+ """Token usage page."""
1730
+ return """
1731
+ <div class="page-header">
1732
+ <div class="page-title">Token Usage</div>
1733
+ <div class="page-subtitle">Token consumption analysis by session.</div>
1734
+ </div>
1735
+ <div class="card">
1736
+ <p>Token usage analysis coming soon.</p>
1737
+ </div>
1738
+ """
1739
+
1740
+
1741
+ def render_alerts():
1742
+ """Alerts page."""
1743
+ return """
1744
+ <div class="page-header">
1745
+ <div class="page-title">Alerts</div>
1746
+ <div class="page-subtitle">Semantic anomaly detection and alerts.</div>
1747
+ </div>
1748
+ <div id="alerts-content" class="loading">
1749
+ <div class="spinner"></div>
1750
+ Loading alerts...
1751
+ </div>
1752
+ """
1753
+
1754
+
1755
+ def render_recommendations():
1756
+ """Recommendations page."""
1757
+ return """
1758
+ <div class="page-header">
1759
+ <div class="page-title">Recommendations</div>
1760
+ <div class="page-subtitle">AI-generated session recommendations.</div>
1761
+ </div>
1762
+ <div class="card">
1763
+ <p>Session recommendations coming soon.</p>
1764
+ </div>
1765
+ """
1766
+
1767
+
1768
+ def render_topics():
1769
+ """Topics page."""
1770
+ return """
1771
+ <div class="page-header">
1772
+ <div class="page-title">Topics</div>
1773
+ <div class="page-subtitle">Semantic topic extraction and tagging.</div>
1774
+ </div>
1775
+ <div class="card">
1776
+ <p>Topic analysis coming soon.</p>
1777
+ </div>
1778
+ """
1779
+
1780
+
1781
+ def render_pipeline():
1782
+ """Pipeline management page."""
1783
+ return """
1784
+ <div class="page-header">
1785
+ <div class="page-title">Pipeline</div>
1786
+ <div class="page-subtitle">Execute and monitor data pipeline commands.</div>
1787
+ </div>
1788
+ <div class="card mb-20">
1789
+ <div class="card-header">Available Commands</div>
1790
+ <div class="button-group">
1791
+ <button class="button button-primary" onclick="runAction('sync')">Full Sync</button>
1792
+ <button class="button button-primary" onclick="runAction('reconstruct')">Reconstruct</button>
1793
+ <button class="button button-primary" onclick="runAction('embed-build')">Build Embeddings</button>
1794
+ </div>
1795
+ <p style="font-size: 12px; color: var(--text-tertiary); margin-top: 10px;">
1796
+ These commands can take several minutes to complete.
1797
+ </p>
1798
+ </div>
1799
+ <div id="action-status" class="hidden">
1800
+ <div class="alert alert-info">
1801
+ <strong>Status:</strong> <span id="status-text">Running...</span>
1802
+ </div>
1803
+ </div>
1804
+ <div class="card mb-20">
1805
+ <div class="card-header">Runtime Services</div>
1806
+ <div id="pmf-services" class="loading">
1807
+ <div class="spinner"></div>
1808
+ Loading runtime services...
1809
+ </div>
1810
+ </div>
1811
+ """
1812
+
1813
+
1814
+ def render_query():
1815
+ """Raw SQL query page."""
1816
+ return """
1817
+ <div class="page-header">
1818
+ <div class="page-title">Raw Query</div>
1819
+ <div class="page-subtitle">Execute SQL queries directly against the database.</div>
1820
+ </div>
1821
+ <div class="card mb-20">
1822
+ <div class="form-group">
1823
+ <label class="form-label">SQL Query</label>
1824
+ <textarea id="query-input" class="form-textarea" placeholder="SELECT * FROM sessions LIMIT 10"></textarea>
1825
+ </div>
1826
+ <button class="button button-primary" onclick="executeQuery()">Execute</button>
1827
+ </div>
1828
+ <div id="query-results" class="hidden">
1829
+ <div class="card">
1830
+ <div class="card-header">Results</div>
1831
+ <div id="results-table"></div>
1832
+ </div>
1833
+ </div>
1834
+ """
1835
+
1836
+
1837
+ PAGE_TEMPLATES = {
1838
+ 'dashboard': json.dumps(render_dashboard()),
1839
+ 'sessions': json.dumps(render_sessions()),
1840
+ 'search': json.dumps(render_search()),
1841
+ 'heat': json.dumps(render_heat()),
1842
+ 'keywords': json.dumps(render_keywords()),
1843
+ 'signals': json.dumps(render_signals()),
1844
+ 'behavior': json.dumps(render_behavior()),
1845
+ 'workspaces': json.dumps(render_workspaces()),
1846
+ 'tools': json.dumps(render_tools()),
1847
+ 'memory': json.dumps(render_memory()),
1848
+ 'tokens': json.dumps(render_tokens()),
1849
+ 'alerts': json.dumps(render_alerts()),
1850
+ 'recommendations': json.dumps(render_recommendations()),
1851
+ 'topics': json.dumps(render_topics()),
1852
+ 'pipeline': json.dumps(render_pipeline()),
1853
+ 'query': json.dumps(render_query())
1854
+ }
1855
+
1856
+ APP_JS = "const PAGE_REGISTRY = {\n"
1857
+ APP_JS += ",\n".join(
1858
+ f" '{name}': () => {template}"
1859
+ for name, template in PAGE_TEMPLATES.items()
1860
+ )
1861
+ APP_JS += "\n};\n\n"
1862
+ APP_JS += """
1863
+ const safeArray = arr => Array.isArray(arr) ? arr : [];
1864
+ // Navigation
1865
+ document.addEventListener('DOMContentLoaded', () => {
1866
+ document.querySelectorAll('.nav-item').forEach(item => {
1867
+ item.addEventListener('click', e => {
1868
+ const page = item.dataset.page;
1869
+ showPage(page);
1870
+ });
1871
+ });
1872
+ showPage('dashboard');
1873
+ });
1874
+
1875
+ function showPage(page) {
1876
+ if (!PAGE_REGISTRY[page]) page = 'dashboard';
1877
+
1878
+ document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
1879
+ document.querySelector(`[data-page="${page}"]`).classList.add('active');
1880
+
1881
+ const renderer = PAGE_REGISTRY[page];
1882
+ document.getElementById('main-content').innerHTML = renderer();
1883
+ initializePage(page);
1884
+
1885
+ window.scrollTo(0, 0);
1886
+ }
1887
+
1888
+ function initializePage(page) {
1889
+ switch (page) {
1890
+ case 'dashboard':
1891
+ initDashboard();
1892
+ break;
1893
+ case 'sessions':
1894
+ initSessions();
1895
+ break;
1896
+ case 'search':
1897
+ initSearch();
1898
+ break;
1899
+ case 'heat':
1900
+ initHeat();
1901
+ break;
1902
+ case 'keywords':
1903
+ initKeywords();
1904
+ break;
1905
+ case 'workspaces':
1906
+ initWorkspaces();
1907
+ break;
1908
+ case 'tools':
1909
+ initTools();
1910
+ break;
1911
+ case 'memory':
1912
+ initMemory();
1913
+ break;
1914
+ case 'alerts':
1915
+ initAlerts();
1916
+ break;
1917
+ case 'pipeline':
1918
+ initPipeline();
1919
+ break;
1920
+ case 'query':
1921
+ initQuery();
1922
+ break;
1923
+ default:
1924
+ break;
1925
+ }
1926
+ }
1927
+
1928
+ function initDashboard() {
1929
+ const container = document.getElementById('dashboard-content');
1930
+ if (!container) return;
1931
+ container.innerHTML = '<div class="spinner"></div> Loading overview...';
1932
+ fetch('/api/overview').then(r => r.json()).then(data => {
1933
+ if (data.error) {
1934
+ container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
1935
+ return;
1936
+ }
1937
+ const s = data.stats;
1938
+ const heatDistribution = safeArray(data.heat_distribution);
1939
+ const keywords = safeArray(data.keywords);
1940
+ const recentSessions = safeArray(data.recent_sessions);
1941
+ const html = `
1942
+ <div class="grid-4">
1943
+ <div class="card">
1944
+ <div class="card-header">Total Sessions</div>
1945
+ <div class="card-value">${s.total_sessions || 0}</div>
1946
+ <div class="card-label">Analyzed</div>
1947
+ </div>
1948
+ <div class="card">
1949
+ <div class="card-header">Avg Heat</div>
1950
+ <div class="card-value">${(s.avg_heat || 0).toFixed(1)}</div>
1951
+ <div class="card-label">Frustration Score</div>
1952
+ </div>
1953
+ <div class="card">
1954
+ <div class="card-header">Critical</div>
1955
+ <div class="card-value">${s.critical_sessions || 0}</div>
1956
+ <div class="card-label">Heat > 50</div>
1957
+ </div>
1958
+ <div class="card">
1959
+ <div class="card-header">Workspaces</div>
1960
+ <div class="card-value">${s.workspace_count || 0}</div>
1961
+ <div class="card-label">Active</div>
1962
+ </div>
1963
+ </div>
1964
+ <div class="card mb-20">
1965
+ <div class="card-header">Heat Distribution</div>
1966
+ <table class="table">
1967
+ <thead><tr><th>Range</th><th>Sessions</th></tr></thead>
1968
+ <tbody>
1969
+ ${heatDistribution.map(h => `<tr><td>${h.range}</td><td>${h.count}</td></tr>`).join('')}
1970
+ </tbody>
1971
+ </table>
1972
+ </div>
1973
+ <div class="card mb-20">
1974
+ <div class="card-header">Top Keywords</div>
1975
+ <div style="display: flex; flex-wrap: wrap; gap: 8px;">
1976
+ ${keywords.map(k => `<span class="badge badge-info">${k.keyword} (${k.total_count})</span>`).join('')}
1977
+ </div>
1978
+ </div>
1979
+ <div class="card">
1980
+ <div class="card-header">Recent Sessions</div>
1981
+ <table class="table">
1982
+ <thead><tr><th>Title</th><th>Heat</th><th>Exchanges</th><th>Date</th></tr></thead>
1983
+ <tbody>
1984
+ ${recentSessions.map(s => `
1985
+ <tr class="clickable session-row" data-session-id="${s.id}">
1986
+ <td class="truncate">${s.title || 'Untitled'}</td>
1987
+ <td>${(s.heat_score || 0).toFixed(1)}</td>
1988
+ <td>${s.exchange_count || 0}</td>
1989
+ <td>${new Date(s.created_at).toLocaleDateString()}</td>
1990
+ </tr>
1991
+ `).join('')}
1992
+ </tbody>
1993
+ </table>
1994
+ </div>
1995
+ `;
1996
+ container.innerHTML = html;
1997
+ document.querySelectorAll('#dashboard-content .session-row').forEach(row => {
1998
+ row.addEventListener('click', () => openSessionDrawer(row.dataset.sessionId));
1999
+ });
2000
+ });
2001
+ }
2002
+
2003
+ function initSessions() {
2004
+ const container = document.getElementById('sessions-content');
2005
+ if (!container) return;
2006
+ container.innerHTML = '<div class="spinner"></div> Loading sessions...';
2007
+ fetch('/api/sessions').then(r => r.json()).then(data => {
2008
+ if (data.error) {
2009
+ container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2010
+ return;
2011
+ }
2012
+ const sessions = safeArray(data.sessions);
2013
+ const html = `
2014
+ <div class="card">
2015
+ <div class="card-header">All Sessions (${data.total || 0})</div>
2016
+ <table class="table">
2017
+ <thead>
2018
+ <tr>
2019
+ <th>ID</th>
2020
+ <th>Title</th>
2021
+ <th>Heat</th>
2022
+ <th>Exchanges</th>
2023
+ <th>Workspace</th>
2024
+ <th>Date</th>
2025
+ </tr>
2026
+ </thead>
2027
+ <tbody>
2028
+ ${sessions.map(s => `
2029
+ <tr class="clickable session-row" data-session-id="${s.id}">
2030
+ <td class="truncate" style="max-width: 150px;">${s.id}</td>
2031
+ <td class="truncate">${s.title || 'Untitled'}</td>
2032
+ <td><strong>${(s.heat_score || 0).toFixed(1)}</strong></td>
2033
+ <td>${s.exchange_count || 0}</td>
2034
+ <td class="truncate">${s.workspace_id || '—'}</td>
2035
+ <td>${new Date(s.created_at).toLocaleDateString()}</td>
2036
+ </tr>
2037
+ `).join('')}
2038
+ </tbody>
2039
+ </table>
2040
+ </div>
2041
+ `;
2042
+ container.innerHTML = html;
2043
+ document.querySelectorAll('#sessions-content .session-row').forEach(row => {
2044
+ row.addEventListener('click', () => openSessionDrawer(row.dataset.sessionId));
2045
+ });
2046
+ });
2047
+ }
2048
+
2049
+ function openSessionDrawer(sessionId) {
2050
+ const drawer = document.getElementById('detail-drawer');
2051
+ const titleEl = document.getElementById('drawer-session-title');
2052
+ const subtitleEl = document.getElementById('drawer-session-subtitle');
2053
+ const body = document.getElementById('drawer-body');
2054
+ const tabButtons = document.querySelectorAll('.drawer-tab');
2055
+
2056
+ titleEl.textContent = 'Loading…';
2057
+ subtitleEl.textContent = 'Fetching session details...';
2058
+ body.innerHTML = '<div class="spinner"></div> Loading session details...';
2059
+ tabButtons.forEach(btn => btn.classList.remove('active'));
2060
+ document.querySelector('.drawer-tab[data-tab="overview"]').classList.add('active');
2061
+ drawer.classList.add('open');
2062
+
2063
+ fetch('/api/session?session_id=' + encodeURIComponent(sessionId)).then(r => r.json()).then(data => {
2064
+ if (data.error) {
2065
+ body.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2066
+ titleEl.textContent = 'Session Error';
2067
+ subtitleEl.textContent = '';
2068
+ window.currentSessionDetail = null;
2069
+ return;
2070
+ }
2071
+ const session = data.session || {};
2072
+
2073
+ window.currentSessionDetail = data;
2074
+
2075
+ titleEl.textContent = session.title || `Session ${session.session_id || sessionId}`;
2076
+ subtitleEl.textContent = `Workspace ${session.workspace_id || '—'} · ${session.created_at ? new Date(session.created_at).toLocaleString() : 'Unknown date'}`; # noqa: E501
2077
+
2078
+ body.innerHTML = renderDrawerTabContent('overview', window.currentSessionDetail);
2079
+ }).catch(err => {
2080
+ body.innerHTML = '<div class="alert alert-danger">Error loading session details.</div>';
2081
+ titleEl.textContent = 'Session Error';
2082
+ subtitleEl.textContent = '';
2083
+ window.currentSessionDetail = null;
2084
+ });
2085
+ }
2086
+
2087
+ function switchDrawerTab(tab) {
2088
+ const tabButtons = document.querySelectorAll('.drawer-tab');
2089
+ tabButtons.forEach(btn => btn.classList.toggle('active', btn.dataset.tab === tab));
2090
+ const body = document.getElementById('drawer-body');
2091
+ if (!window.currentSessionDetail) {
2092
+ body.innerHTML = '<div class="spinner"></div> Loading session details...';
2093
+ return;
2094
+ }
2095
+ body.innerHTML = renderDrawerTabContent(tab, window.currentSessionDetail);
2096
+ }
2097
+
2098
+ function renderDrawerTabContent(tab, data) {
2099
+ const session = data.session || {};
2100
+ const analysis = data.analysis || {};
2101
+ const exchanges = Array.isArray(data.exchanges) ? data.exchanges : [];
2102
+ const signals = Array.isArray(data.signals) ? data.signals : [];
2103
+ const toolCalls = Array.isArray(data.tool_calls) ? data.tool_calls : [];
2104
+ const vfsEntries = Array.isArray(data.vfs) ? data.vfs : [];
2105
+ const alerts = Array.isArray(data.alerts) ? data.alerts : [];
2106
+
2107
+ const heatScore = analysis.heat_score !== undefined && analysis.heat_score !== null ? analysis.heat_score : 'N/A';
2108
+ const metadataSection = `
2109
+ <div class="drawer-section">
2110
+ <h3>Session Metadata</h3>
2111
+ <div class="session-panel">
2112
+ ${[
2113
+ ['Session ID', session.session_id || 'Unknown'],
2114
+ ['Title', session.title || 'Untitled'],
2115
+ ['Workspace', session.workspace_id || '—'],
2116
+ ['Requests', session.request_count || '—'],
2117
+ ['State', session.response_state || '—'],
2118
+ ['Location', session.initial_location || '—'],
2119
+ ['Heat Score', `<span style="color: ${heatScore !== 'N/A' && heatScore >= 50 ? 'var(--danger)' : 'var(--accent)'};">${heatScore}</span>`], # noqa: E501
2120
+ ['Created At', session.created_at ? new Date(session.created_at).toLocaleString() : 'Unknown']
2121
+ ].map(([label, value]) => `
2122
+ <div class="data-row">
2123
+ <div class="data-label">${label}</div>
2124
+ <div class="data-value">${value}</div>
2125
+ </div>
2126
+ `).join('')}
2127
+ </div>
2128
+ </div>
2129
+ `;
2130
+
2131
+ const sanitize = text => String(text || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
2132
+ const sessionSummary = sanitize(session.summary || analysis.summary || '');
2133
+ const turnPoint = analysis.turning_point_text ? sanitize(analysis.turning_point_text.substring(0, 300)) + (analysis.turning_point_text.length > 300 ? '…' : '') : ''; # noqa: E501
2134
+
2135
+ if (tab === 'overview') {
2136
+ const chatCount = exchanges.length;
2137
+ const signalCount = signals.length;
2138
+ const toolCallsCount = toolCalls.length;
2139
+ const fileCount = vfsEntries.length;
2140
+ const alertCount = alerts.length;
2141
+ return `
2142
+ ${metadataSection}
2143
+ <div class="drawer-section">
2144
+ <h3>Session Snapshot</h3>
2145
+ <div class="session-panel">
2146
+ ${[
2147
+ ['Chat Turns', chatCount],
2148
+ ['Signals', signalCount],
2149
+ ['Tool Calls', toolCallsCount],
2150
+ ['Files', fileCount],
2151
+ ['Alerts', `<span style="color: ${alertCount > 0 ? 'var(--danger)' : 'var(--success)'};">${alertCount}</span>`]
2152
+ ].map(([label, value]) => `
2153
+ <div class="data-row">
2154
+ <div class="data-label">${label}</div>
2155
+ <div class="data-value">${value}</div>
2156
+ </div>
2157
+ `).join('')}
2158
+ </div>
2159
+ </div>
2160
+ ${sessionSummary ? `<div class="drawer-section">
2161
+ <h3>Summary</h3>
2162
+ <div class="session-block">${sessionSummary}</div>
2163
+ </div>` : ''}
2164
+ ${turnPoint ? `<div class="drawer-section">
2165
+ <h3>Turning Point</h3>
2166
+ <div class="session-block">${turnPoint}</div>
2167
+ </div>` : ''}
2168
+ `;
2169
+ }
2170
+
2171
+ if (tab === 'analysis') {
2172
+ const details = [
2173
+ ['Heat Score', heatScore],
2174
+ ['Peak Heat', analysis.peak_heat],
2175
+ ['Final Heat', analysis.final_heat],
2176
+ ['Frustrations', analysis.total_frustrations],
2177
+ ['Corrections', analysis.total_corrections],
2178
+ ['Pre-corrections', analysis.total_pre_corrections],
2179
+ ['Redirects', analysis.total_redirects],
2180
+ ['Tool Calls', analysis.total_tool_calls],
2181
+ ['Compactions', analysis.compaction_count],
2182
+ ['Token Prompt', analysis.total_tokens_prompt],
2183
+ ['Token Completion', analysis.total_tokens_completion],
2184
+ ['Token Cached', analysis.total_tokens_cached],
2185
+ ['Duration (min)', analysis.session_duration_min],
2186
+ ['Model IDs', analysis.model_ids],
2187
+ ['Analyzed At', analysis.analyzed_at],
2188
+ ['Saved Session', analysis.saved_session],
2189
+ ['Clean Run', analysis.clean_run]
2190
+ ];
2191
+ return `
2192
+ ${metadataSection}
2193
+ <div class="drawer-section">
2194
+ <h3>Analysis Details</h3>
2195
+ <div class="session-panel">
2196
+ ${details.filter(([label, value]) => value !== undefined && value !== null && value !== '').map(([label, value]) => `
2197
+ <div class="data-row">
2198
+ <div class="data-label">${label}</div>
2199
+ <div class="data-value">${typeof value === 'number' ? value : String(value)}</div>
2200
+ </div>
2201
+ `).join('')}
2202
+ </div>
2203
+ </div>
2204
+ `;
2205
+ }
2206
+
2207
+ if (tab === 'chat') {
2208
+ const exchangesHtml = exchanges.length ? `<div class="chat-thread">${exchanges.map((e, i) => `
2209
+ <div class="chat-message">
2210
+ <div class="chat-message-header">
2211
+ <div>
2212
+ <div class="chat-role">Turn ${e.exchange_index || i + 1}</div>
2213
+ <div class="chat-meta">${e.created_at ? new Date(e.created_at).toLocaleString() : 'Unknown'}</div>
2214
+ </div>
2215
+ <div class="chat-role">Exchange</div>
2216
+ </div>
2217
+ <div class="chat-message-block user">
2218
+ <div class="chat-message-label">User</div>
2219
+ <div>${(e.user_input || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
2220
+ </div>
2221
+ <div class="chat-message-block assistant">
2222
+ <div class="chat-message-label">Assistant</div>
2223
+ <div>${(e.assistant_response || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div>
2224
+ </div>
2225
+ </div>
2226
+ `).join('')}</div>` : '<div class="alert alert-info">No exchanges found for this session.</div>';
2227
+ return `
2228
+ ${metadataSection}
2229
+ <div class="drawer-section">
2230
+ <h3>Chat History (${exchanges.length} turns)</h3>
2231
+ </div>
2232
+ ${exchangesHtml}
2233
+ `;
2234
+ }
2235
+
2236
+ if (tab === 'tools') {
2237
+ const embeddedCalls = exchanges.flatMap(e => {
2238
+ if (!e.tool_calls) return [];
2239
+ try {
2240
+ const parsed = JSON.parse(e.tool_calls);
2241
+ return Array.isArray(parsed) ? parsed.map(call => ({...call, created_at: e.created_at, exchange_index: e.exchange_index})) : [];
2242
+ } catch (err) {
2243
+ return [];
2244
+ }
2245
+ });
2246
+ const allToolCalls = toolCalls.length ? toolCalls : embeddedCalls;
2247
+ const toolsHtml = allToolCalls.length ? `<table class="table"><thead><tr><th>#</th><th>Tool</th><th>Exchange</th><th>When</th></tr></thead><tbody>${allToolCalls.map((call, i) => `<tr><td>${i + 1}</td><td>${call.tool_name || call.name || 'Tool'}</td><td>${call.exchange_index || ''}</td><td>${call.created_at ? new Date(call.created_at).toLocaleString() : ''}</td></tr><tr><td colspan="4"><div class="code-block" style="margin: 0; padding: 12px;">${(call.arguments_json || call.arguments || call.args || 'No arguments').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div></td></tr>`).join('')}</tbody></table>` : '<div class="alert alert-info">No tool calls recorded for this session.</div>'; # noqa: E501
2248
+ return `
2249
+ ${metadataSection}
2250
+ <div class="drawer-section">
2251
+ <h3>Tool Calls (${allToolCalls.length} total)</h3>
2252
+ ${toolsHtml}
2253
+ </div>
2254
+ `;
2255
+ }
2256
+
2257
+ if (tab === 'signals') {
2258
+ const summaryHtml = Array.isArray(data.signal_summary) && data.signal_summary.length ? `
2259
+ <div class="drawer-section">
2260
+ <h3>Signal Summary</h3>
2261
+ <table class="table">
2262
+ <thead>
2263
+ <tr><th>Signal Type</th><th>Count</th></tr>
2264
+ </thead>
2265
+ <tbody>
2266
+ ${data.signal_summary.map(s => `<tr><td>${s.signal_type}</td><td>${s.count}</td></tr>`).join('')}
2267
+ </tbody>
2268
+ </table>
2269
+ </div>
2270
+ ` : '';
2271
+ const signalsHtml = signals.length ? `<table class="table"><thead><tr><th>#</th><th>Signal</th><th>Created</th><th>Details</th></tr></thead><tbody>${signals.map((s, i) => `<tr><td>${i + 1}</td><td>${s.signal_type || s.matched_keyword || 'Signal'}</td><td>${s.created_at ? new Date(s.created_at).toLocaleString() : ''}</td><td>${(s.signal_text || s.user_message || s.matched_keyword || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</td></tr>`).join('')}</tbody></table>` : '<div class="alert alert-info">No exchange signals available.</div>'; # noqa: E501
2272
+ return `
2273
+ ${metadataSection}
2274
+ ${summaryHtml}
2275
+ <div class="drawer-section">
2276
+ <h3>Signal Details (${signals.length} total)</h3>
2277
+ ${signalsHtml}
2278
+ </div>
2279
+ `;
2280
+ }
2281
+
2282
+ if (tab === 'files') {
2283
+ const fileHtml = vfsEntries.length ? `<table class="table"><thead><tr><th>#</th><th>File</th><th>Type</th><th>Size</th><th>Path</th></tr></thead><tbody>${vfsEntries.map((file, i) => `<tr><td>${i + 1}</td><td>${file.filename || file.source_path || file.source_type}</td><td>${file.content_type || 'unknown'}</td><td>${file.size_bytes ? (file.size_bytes / 1024).toFixed(2) + ' KB' : 'unknown'}</td><td class="truncate">${file.source_path || ''}</td></tr>`).join('')}</tbody></table>` : '<div class="alert alert-info">No session files found.</div>'; # noqa: E501
2284
+ return `
2285
+ ${metadataSection}
2286
+ <div class="drawer-section">
2287
+ <h3>Session Files (${vfsEntries.length} total)</h3>
2288
+ ${fileHtml}
2289
+ </div>
2290
+ `;
2291
+ }
2292
+
2293
+ if (tab === 'alerts') {
2294
+ const alertsHtml = alerts.length ? `<table class="table"><thead><tr><th>#</th><th>Alert</th><th>Severity</th><th>Created</th></tr></thead><tbody>${alerts.map((alert, i) => `<tr><td>${i + 1}</td><td>${alert.alert_type || 'Alert'}<div class="alert-text">${(alert.message || '').replace(/</g, '&lt;').replace(/>/g, '&gt;')}</div></td><td>${alert.severity || 'unknown'}</td><td>${alert.created_at ? new Date(alert.created_at).toLocaleString() : ''}</td></tr>`).join('')}</tbody></table>` : '<div class="alert alert-info">No alerts recorded for this session.</div>'; # noqa: E501
2295
+ return `
2296
+ ${metadataSection}
2297
+ <div class="drawer-section">
2298
+ <h3>Alerts (${alerts.length} total)</h3>
2299
+ ${alertsHtml}
2300
+ </div>
2301
+ `;
2302
+ }
2303
+
2304
+ if (tab === 'raw') {
2305
+ const raw = JSON.stringify(data, null, 2).replace(/</g, '&lt;').replace(/>/g, '&gt;');
2306
+ return `
2307
+ <div class="drawer-section">
2308
+ <h3>Raw Session Payload</h3>
2309
+ <pre class="code-block" style="white-space: pre-wrap; word-break: break-word;">${raw}</pre>
2310
+ </div>
2311
+ `;
2312
+ }
2313
+
2314
+ return '<p>Tab content unavailable.</p>';
2315
+ }
2316
+
2317
+ function closeSessionDrawer() {
2318
+ const drawer = document.getElementById('detail-drawer');
2319
+ drawer.classList.remove('open');
2320
+ }
2321
+
2322
+ function initSearch() {
2323
+ const results = document.getElementById('search-results');
2324
+ if (!results) return;
2325
+ results.style.display = 'none';
2326
+ const input = document.getElementById('search-input');
2327
+ if (input) {
2328
+ input.addEventListener('keypress', e => {
2329
+ if (e.key === 'Enter') performSearch();
2330
+ });
2331
+ }
2332
+ }
2333
+
2334
+ function initHeat() {
2335
+ const container = document.getElementById('heat-content');
2336
+ if (!container) return;
2337
+ container.innerHTML = '<div class="spinner"></div> Loading heat analysis...';
2338
+ fetch('/api/overview').then(r => r.json()).then(data => {
2339
+ container.innerHTML = '<div class="card">Heat data visualization placeholder</div>';
2340
+ });
2341
+ }
2342
+
2343
+ function initKeywords() {
2344
+ const container = document.getElementById('keywords-content');
2345
+ if (!container) return;
2346
+ container.innerHTML = '<div class="spinner"></div> Loading keywords...';
2347
+ fetch('/api/overview').then(r => r.json()).then(data => {
2348
+ const html = `
2349
+ <div class="card">
2350
+ <div class="card-header">Top Keywords</div>
2351
+ <div style="display: grid; grid-template-columns: repeat(auto-fill, minmax(150px, 1fr)); gap: 10px;">
2352
+ ${data.keywords.map(k => `
2353
+ <div style="background: var(--bg-secondary); padding: 15px; border-radius: 6px; text-align: center;">
2354
+ <div style="font-weight: 600; color: var(--accent);">${k.keyword}</div>
2355
+ <div style="font-size: 20px; font-weight: 700; color: var(--text-primary);">${k.total_count}</div>
2356
+ </div>
2357
+ `).join('')}
2358
+ </div>
2359
+ </div>
2360
+ `;
2361
+ container.innerHTML = html;
2362
+ });
2363
+ }
2364
+
2365
+ function initWorkspaces() {
2366
+ const container = document.getElementById('workspaces-content');
2367
+ if (!container) return;
2368
+ container.innerHTML = '<div class="spinner"></div> Loading workspaces...';
2369
+ fetch('/api/workspaces').then(r => r.json()).then(data => {
2370
+ if (data.error) {
2371
+ container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2372
+ return;
2373
+ }
2374
+ const html = `
2375
+ <div class="card">
2376
+ <div class="card-header">All Workspaces</div>
2377
+ <table class="table">
2378
+ <thead>
2379
+ <tr><th>Workspace</th><th>Sessions</th><th>Last Activity</th></tr>
2380
+ </thead>
2381
+ <tbody>
2382
+ ${data.workspaces.map(w => `
2383
+ <tr>
2384
+ <td class="truncate">${w.workspace_id}</td>
2385
+ <td>${w.session_count}</td>
2386
+ <td>${new Date(w.last_session).toLocaleDateString()}</td>
2387
+ </tr>
2388
+ `).join('')}
2389
+ </tbody>
2390
+ </table>
2391
+ </div>
2392
+ `;
2393
+ container.innerHTML = html;
2394
+ });
2395
+ }
2396
+
2397
+ function initTools() {
2398
+ const container = document.getElementById('tools-content');
2399
+ if (!container) return;
2400
+ container.innerHTML = '<div class="spinner"></div> Searching...';
2401
+ }
2402
+
2403
+ function initMemory() {
2404
+ const container = document.getElementById('memory-content');
2405
+ if (!container) return;
2406
+ container.innerHTML = '<div class="spinner"></div> Loading memory...';
2407
+ fetch('/api/memory').then(r => r.json()).then(data => {
2408
+ if (data.error) {
2409
+ container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2410
+ return;
2411
+ }
2412
+ const html = `
2413
+ <div class="card">
2414
+ <div class="card-header">Memory Files</div>
2415
+ <table class="table">
2416
+ <thead>
2417
+ <tr><th>Name</th><th>Size</th><th>Created</th><th>Updated</th></tr>
2418
+ </thead>
2419
+ <tbody>
2420
+ ${data.memory.map(m => `
2421
+ <tr>
2422
+ <td class="truncate">${m.name}</td>
2423
+ <td>${(m.size / 1024).toFixed(1)}KB</td>
2424
+ <td>${new Date(m.created_at).toLocaleDateString()}</td>
2425
+ <td>${new Date(m.updated_at).toLocaleDateString()}</td>
2426
+ </tr>
2427
+ `).join('')}
2428
+ </tbody>
2429
+ </table>
2430
+ </div>
2431
+ `;
2432
+ container.innerHTML = html;
2433
+ });
2434
+ }
2435
+
2436
+ function initAlerts() {
2437
+ const container = document.getElementById('alerts-content');
2438
+ if (!container) return;
2439
+ container.innerHTML = '<div class="spinner"></div> Loading alerts...';
2440
+ fetch('/api/alerts').then(r => r.json()).then(data => {
2441
+ if (data.error) {
2442
+ container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2443
+ return;
2444
+ }
2445
+ const html = `
2446
+ <div class="card">
2447
+ <div class="card-header">Anomaly Alerts</div>
2448
+ <table class="table">
2449
+ <thead>
2450
+ <tr>
2451
+ <th>Type</th>
2452
+ <th>Session</th>
2453
+ <th>Message</th>
2454
+ <th>Severity</th>
2455
+ <th>Date</th>
2456
+ </tr>
2457
+ </thead>
2458
+ <tbody>
2459
+ ${data.alerts.map(a => `
2460
+ <tr>
2461
+ <td>${a.alert_type}</td>
2462
+ <td class="truncate">${a.session_title}</td>
2463
+ <td class="truncate">${a.message}</td>
2464
+ <td><span class="badge badge-${a.severity === 'high' ? 'danger' : 'warning'}">${a.severity}</span></td>
2465
+ <td>${new Date(a.created_at).toLocaleDateString()}</td>
2466
+ </tr>
2467
+ `).join('')}
2468
+ </tbody>
2469
+ </table>
2470
+ </div>
2471
+ `;
2472
+ container.innerHTML = html;
2473
+ });
2474
+ }
2475
+
2476
+ function initPipeline() {
2477
+ const status = document.getElementById('action-status');
2478
+ if (status) {
2479
+ status.classList.add('hidden');
2480
+ }
2481
+
2482
+ const container = document.getElementById('pmf-services');
2483
+ if (!container) return;
2484
+ container.innerHTML = '<div class="spinner"></div> Loading runtime services...';
2485
+ fetch('/api/pmf/services').then(r => r.json()).then(data => {
2486
+ if (data.error) {
2487
+ container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2488
+ return;
2489
+ }
2490
+ const html = `
2491
+ <table class="table">
2492
+ <thead>
2493
+ <tr>
2494
+ <th>Service</th>
2495
+ <th>Status</th>
2496
+ <th>PID</th>
2497
+ <th>Updated</th>
2498
+ <th>Actions</th>
2499
+ </tr>
2500
+ </thead>
2501
+ <tbody>
2502
+ ${data.services.map(s => `
2503
+ <tr>
2504
+ <td><strong>${s.label}</strong><div class="truncate" style="font-size:12px;color:var(--text-tertiary);">${s.description}</div></td> # noqa: E501
2505
+ <td>${s.status}</td>
2506
+ <td>${s.pid || '—'}</td>
2507
+ <td>${s.updated_at || '—'}</td>
2508
+ <td>
2509
+ ${s.allowed_actions.includes('start') ? `<button class="button button-secondary small" onclick="runPmfServiceAction('${s.service_id}','start')">Start</button>` : ''} # noqa: E501
2510
+ ${s.allowed_actions.includes('stop') ? `<button class="button button-secondary small" onclick="runPmfServiceAction('${s.service_id}','stop')">Stop</button>` : ''} # noqa: E501
2511
+ ${s.allowed_actions.includes('restart') ? `<button class="button button-secondary small" onclick="runPmfServiceAction('${s.service_id}','restart')">Restart</button>` : ''} # noqa: E501
2512
+ </td>
2513
+ </tr>
2514
+ `).join('')}
2515
+ </tbody>
2516
+ </table>
2517
+ `;
2518
+ container.innerHTML = html;
2519
+ });
2520
+ }
2521
+
2522
+ function runPmfServiceAction(service, action) {
2523
+ const status = document.getElementById('action-status');
2524
+ if (status) {
2525
+ status.classList.remove('hidden');
2526
+ document.getElementById('status-text').innerHTML = action + ' ' + service + '...';
2527
+ }
2528
+ fetch('/api/pmf/service', {
2529
+ method: 'POST',
2530
+ headers: {'Content-Type': 'application/json'},
2531
+ body: JSON.stringify({service: service, action: action})
2532
+ })
2533
+ .then(r => r.json())
2534
+ .then(data => {
2535
+ if (data.error) {
2536
+ if (status) document.getElementById('status-text').innerHTML = 'Error: ' + data.error;
2537
+ return;
2538
+ }
2539
+ if (status) document.getElementById('status-text').innerHTML = data.message || 'Command executed';
2540
+ setTimeout(initPipeline, 1000);
2541
+ });
2542
+ }
2543
+
2544
+ function initQuery() {
2545
+ const results = document.getElementById('query-results');
2546
+ if (!results) return;
2547
+ results.classList.add('hidden');
2548
+ }
2549
+
2550
+ function performSearch() {
2551
+ const query = document.getElementById('search-input').value;
2552
+ if (!query) return alert('Enter a search query');
2553
+ const results = document.getElementById('search-results');
2554
+ results.style.display = 'block';
2555
+ results.innerHTML = '<div class="spinner"></div> Searching...';
2556
+ fetch('/api/search?q=' + encodeURIComponent(query))
2557
+ .then(r => r.json())
2558
+ .then(data => {
2559
+ if (data.error) {
2560
+ results.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2561
+ return;
2562
+ }
2563
+ const html = `
2564
+ <div class="card">
2565
+ <div class="card-header">Results for "${data.query}" (${data.count})</div>
2566
+ <table class="table">
2567
+ <thead>
2568
+ <tr>
2569
+ <th>Session</th>
2570
+ <th>Exchange</th>
2571
+ <th>User Input</th>
2572
+ <th>Heat</th>
2573
+ </tr>
2574
+ </thead>
2575
+ <tbody>
2576
+ ${data.results.slice(0, 50).map(r => `
2577
+ <tr>
2578
+ <td class="truncate">${r.title || 'Untitled'}</td>
2579
+ <td>${r.relevance}</td>
2580
+ <td class="truncate">${r.user_input || '—'}</td>
2581
+ <td>${(r.heat_score || 0).toFixed(1)}</td>
2582
+ </tr>
2583
+ `).join('')}
2584
+ </tbody>
2585
+ </table>
2586
+ </div>
2587
+ `;
2588
+ results.innerHTML = html;
2589
+ });
2590
+ }
2591
+
2592
+ function searchTools() {
2593
+ const query = document.getElementById('tool-search').value;
2594
+ const container = document.getElementById('tools-content');
2595
+ if (!container) return;
2596
+ container.style.display = 'block';
2597
+ container.innerHTML = '<div class="spinner"></div> Searching...';
2598
+ fetch('/api/tools?q=' + encodeURIComponent(query))
2599
+ .then(r => r.json())
2600
+ .then(data => {
2601
+ if (data.error) {
2602
+ container.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2603
+ return;
2604
+ }
2605
+ const html = `
2606
+ <div class="card">
2607
+ <div class="card-header">Tool Calls (${data.count})</div>
2608
+ <table class="table">
2609
+ <thead>
2610
+ <tr>
2611
+ <th>Tool Name</th>
2612
+ <th>Session</th>
2613
+ <th>Arguments</th>
2614
+ <th>Date</th>
2615
+ </tr>
2616
+ </thead>
2617
+ <tbody>
2618
+ ${data.tool_calls.slice(0, 50).map(t => `
2619
+ <tr>
2620
+ <td><strong>${t.tool_name}</strong></td>
2621
+ <td class="truncate">${t.session_title}</td>
2622
+ <td class="truncate" style="max-width: 300px;">${t.arguments || '—'}</td>
2623
+ <td>${new Date(t.created_at).toLocaleDateString()}</td>
2624
+ </tr>
2625
+ `).join('')}
2626
+ </tbody>
2627
+ </table>
2628
+ </div>
2629
+ `;
2630
+ container.innerHTML = html;
2631
+ });
2632
+ }
2633
+
2634
+ function runAction(action) {
2635
+ const status = document.getElementById('action-status');
2636
+ if (!status) return;
2637
+ status.classList.remove('hidden');
2638
+ document.getElementById('status-text').innerHTML = action + ' started...';
2639
+ fetch('/api/action', {
2640
+ method: 'POST',
2641
+ headers: {'Content-Type': 'application/json'},
2642
+ body: JSON.stringify({action: action})
2643
+ })
2644
+ .then(r => r.json())
2645
+ .then(data => {
2646
+ document.getElementById('status-text').innerHTML = data.message || 'Command executed';
2647
+ });
2648
+ }
2649
+
2650
+ function executeQuery() {
2651
+ const sql = document.getElementById('query-input').value;
2652
+ if (!sql) return alert('Enter a SQL query');
2653
+ const results = document.getElementById('query-results');
2654
+ if (!results) return;
2655
+ results.classList.remove('hidden');
2656
+ results.innerHTML = '<div class="spinner"></div> Running query...';
2657
+ fetch('/api/query', {
2658
+ method: 'POST',
2659
+ headers: {'Content-Type': 'application/json'},
2660
+ body: JSON.stringify({sql: sql})
2661
+ })
2662
+ .then(r => r.json())
2663
+ .then(data => {
2664
+ if (data.error) {
2665
+ results.innerHTML = '<div class="alert alert-danger">Error: ' + data.error + '</div>';
2666
+ return;
2667
+ }
2668
+ let html = '<table class="table"><thead><tr>';
2669
+ if (data.rows && data.rows.length > 0) {
2670
+ Object.keys(data.rows[0]).forEach(k => html += '<th>' + k + '</th>');
2671
+ html += '</tr></thead><tbody>';
2672
+ data.rows.forEach(row => {
2673
+ html += '<tr>';
2674
+ Object.values(row).forEach(v => html += '<td class="truncate">' + v + '</td>');
2675
+ html += '</tr>';
2676
+ });
2677
+ }
2678
+ html += '</tbody></table>';
2679
+ document.getElementById('results-table').innerHTML = html;
2680
+ });
2681
+ }
2682
+ """
2683
+
2684
+
2685
+ def application(environ, start_response):
2686
+ """WSGI application."""
2687
+ method = environ['REQUEST_METHOD']
2688
+ path = environ['PATH_INFO']
2689
+ query = parse_qs(environ.get('QUERY_STRING', ''))
2690
+
2691
+ try:
2692
+ if path == '/':
2693
+ response = INDEX_HTML.replace('{{STYLE_CSS}}', STYLE_CSS).replace('{{APP_JS}}', APP_JS).encode('utf-8')
2694
+ start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])
2695
+ return [response]
2696
+
2697
+ elif path == '/api/overview':
2698
+ data = get_overview()
2699
+ response = json.dumps(data).encode('utf-8')
2700
+ start_response('200 OK', [('Content-Type', 'application/json')])
2701
+ return [response]
2702
+
2703
+ elif path == '/api/sessions':
2704
+ limit = int(query.get('limit', ['50'])[0])
2705
+ offset = int(query.get('offset', ['0'])[0])
2706
+ data = get_sessions(limit, offset)
2707
+ response = json.dumps(data).encode('utf-8')
2708
+ start_response('200 OK', [('Content-Type', 'application/json')])
2709
+ return [response]
2710
+
2711
+ elif path == '/api/search':
2712
+ q = query.get('q', [''])[0]
2713
+ data = get_search_results(q)
2714
+ response = json.dumps(data).encode('utf-8')
2715
+ start_response('200 OK', [('Content-Type', 'application/json')])
2716
+ return [response]
2717
+
2718
+ elif path == '/api/workspaces':
2719
+ data = get_workspaces()
2720
+ response = json.dumps(data).encode('utf-8')
2721
+ start_response('200 OK', [('Content-Type', 'application/json')])
2722
+ return [response]
2723
+
2724
+ elif path == '/api/session':
2725
+ session_id = query.get('session_id', [''])[0]
2726
+ data = get_session_detail(session_id)
2727
+ response = json.dumps(data).encode('utf-8')
2728
+ start_response('200 OK', [('Content-Type', 'application/json')])
2729
+ return [response]
2730
+
2731
+ elif path == '/api/tools':
2732
+ q = query.get('q', [''])[0]
2733
+ data = get_tool_calls(q if q else None)
2734
+ response = json.dumps(data).encode('utf-8')
2735
+ start_response('200 OK', [('Content-Type', 'application/json')])
2736
+ return [response]
2737
+
2738
+ elif path == '/api/memory':
2739
+ data = get_memory()
2740
+ response = json.dumps(data).encode('utf-8')
2741
+ start_response('200 OK', [('Content-Type', 'application/json')])
2742
+ return [response]
2743
+
2744
+ elif path == '/api/alerts':
2745
+ data = get_alerts()
2746
+ response = json.dumps(data).encode('utf-8')
2747
+ start_response('200 OK', [('Content-Type', 'application/json')])
2748
+ return [response]
2749
+
2750
+ elif path == '/api/action' and method == 'POST':
2751
+ body = environ['wsgi.input'].read()
2752
+ payload = json.loads(body.decode('utf-8'))
2753
+ action = payload.get('action', '')
2754
+
2755
+ action_id = f"{action}_{int(time.time() * 1000)}"
2756
+ thread = threading.Thread(target=run_action_background, args=(action_id, action))
2757
+ thread.daemon = True
2758
+ thread.start()
2759
+
2760
+ response = json.dumps({
2761
+ "ok": True,
2762
+ "action": action,
2763
+ "action_id": action_id,
2764
+ "message": f"Started {action}..."
2765
+ }).encode('utf-8')
2766
+ start_response('200 OK', [('Content-Type', 'application/json')])
2767
+ return [response]
2768
+
2769
+ elif path == '/api/query' and method == 'POST':
2770
+ body = environ['wsgi.input'].read()
2771
+ payload = json.loads(body.decode('utf-8'))
2772
+ sql = payload.get('sql', '')
2773
+
2774
+ rows = query_rows(sql)
2775
+ response = json.dumps({"rows": rows}).encode('utf-8')
2776
+ start_response('200 OK', [('Content-Type', 'application/json')])
2777
+ return [response]
2778
+
2779
+ elif path == '/api/pmf/services' and method == 'GET':
2780
+ try:
2781
+ services = kernel.services()
2782
+ response = json.dumps({"services": services}).encode('utf-8')
2783
+ start_response('200 OK', [('Content-Type', 'application/json')])
2784
+ return [response]
2785
+ except Exception as e:
2786
+ response = json.dumps({"error": str(e)}).encode('utf-8')
2787
+ start_response('500 Internal Server Error', [('Content-Type', 'application/json')])
2788
+ return [response]
2789
+
2790
+ elif path == '/api/pmf/service' and method == 'POST':
2791
+ body = environ['wsgi.input'].read()
2792
+ payload = json.loads(body.decode('utf-8'))
2793
+ service_id = payload.get('service')
2794
+ action = payload.get('action')
2795
+ if not service_id or not action:
2796
+ raise ValueError('service and action are required')
2797
+ try:
2798
+ if action == 'start':
2799
+ result = kernel.start_service(service_id, options=payload.get('options', {}))
2800
+ elif action == 'stop':
2801
+ result = kernel.stop_service(service_id)
2802
+ elif action == 'restart':
2803
+ result = kernel.restart_service(service_id, options=payload.get('options', {}))
2804
+ else:
2805
+ raise ValueError('unsupported action: ' + action)
2806
+ response = json.dumps({
2807
+ "ok": True,
2808
+ "service": service_id,
2809
+ "action": action,
2810
+ "message": f"{action} requested for {service_id}",
2811
+ "result": result,
2812
+ }).encode('utf-8')
2813
+ start_response('200 OK', [('Content-Type', 'application/json')])
2814
+ return [response]
2815
+ except Exception as e:
2816
+ response = json.dumps({"error": str(e)}).encode('utf-8')
2817
+ start_response('500 Internal Server Error', [('Content-Type', 'application/json')])
2818
+ return [response]
2819
+
2820
+ else:
2821
+ response = b'Not Found'
2822
+ start_response('404 Not Found', [('Content-Type', 'text/plain')])
2823
+ return [response]
2824
+
2825
+ except Exception as e:
2826
+ traceback.print_exc()
2827
+ response = json.dumps({"error": str(e)}).encode('utf-8')
2828
+ start_response('500 Internal Server Error', [('Content-Type', 'application/json')])
2829
+ return [response]
2830
+
2831
+
2832
+ def start_server(host='127.0.0.1', port=10001):
2833
+ """Start WSGI server."""
2834
+ print(f"Starting Code Data Ark Intelligence Portal at http://{host}:{port}")
2835
+ print("Press Ctrl+C to stop.")
2836
+
2837
+ # Use custom server to allow address reuse
2838
+ class ReusableTCPServer(WSGIServer):
2839
+ def server_bind(self):
2840
+ self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
2841
+ super().server_bind()
2842
+
2843
+ httpd = make_server(host, port, application, server_class=ReusableTCPServer)
2844
+ httpd.serve_forever()
2845
+
2846
+
2847
+ if __name__ == '__main__':
2848
+ start_server()