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/__init__.py +3 -0
- cda/kernel/__init__.py +0 -0
- cda/kernel/control_db.py +151 -0
- cda/kernel/pmf_kernel.py +364 -0
- cda/kernel/selfcheck.py +299 -0
- cda/pipeline/__init__.py +0 -0
- cda/pipeline/embed.py +694 -0
- cda/pipeline/extract.py +1064 -0
- cda/pipeline/ingest.py +673 -0
- cda/pipeline/parse_edits.py +250 -0
- cda/pipeline/reconstruct.py +536 -0
- cda/pipeline/watcher.py +783 -0
- cda/ui/__init__.py +0 -0
- cda/ui/cli.py +2587 -0
- cda/ui/web.py +2848 -0
- code_data_ark-2.0.2.dist-info/METADATA +495 -0
- code_data_ark-2.0.2.dist-info/RECORD +20 -0
- code_data_ark-2.0.2.dist-info/WHEEL +4 -0
- code_data_ark-2.0.2.dist-info/entry_points.txt +2 -0
- code_data_ark-2.0.2.dist-info/licenses/license +21 -0
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, '<').replace(/>/g, '>');
|
|
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, '<').replace(/>/g, '>')}</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, '<').replace(/>/g, '>')}</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, '<').replace(/>/g, '>')}</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, '<').replace(/>/g, '>')}</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, '<').replace(/>/g, '>')}</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, '<').replace(/>/g, '>');
|
|
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()
|