@oculisecurity/cli 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. package/LICENSE.txt +201 -0
  2. package/README.md +67 -0
  3. package/dist/cli.d.ts +18 -0
  4. package/dist/cli.js +565 -0
  5. package/dist/commands/init.d.ts +14 -0
  6. package/dist/commands/init.js +135 -0
  7. package/dist/commands/report.d.ts +33 -0
  8. package/dist/commands/report.js +145 -0
  9. package/dist/commands/serve.d.ts +27 -0
  10. package/dist/commands/serve.js +163 -0
  11. package/dist/commands/tail.d.ts +7 -0
  12. package/dist/commands/tail.js +211 -0
  13. package/dist/commands/uninstall.d.ts +13 -0
  14. package/dist/commands/uninstall.js +111 -0
  15. package/dist/config.d.ts +17 -0
  16. package/dist/config.js +90 -0
  17. package/dist/index.d.ts +1 -0
  18. package/dist/index.js +35 -0
  19. package/dist/init.d.ts +9 -0
  20. package/dist/init.js +50 -0
  21. package/dist/install/claude-code.d.ts +13 -0
  22. package/dist/install/claude-code.js +118 -0
  23. package/dist/install/cursor.d.ts +13 -0
  24. package/dist/install/cursor.js +119 -0
  25. package/dist/install/detect.d.ts +5 -0
  26. package/dist/install/detect.js +64 -0
  27. package/dist/middleware/auth.d.ts +15 -0
  28. package/dist/middleware/auth.js +116 -0
  29. package/dist/routes/adapters/claude-code.d.ts +38 -0
  30. package/dist/routes/adapters/claude-code.js +125 -0
  31. package/dist/routes/adapters/cursor.d.ts +21 -0
  32. package/dist/routes/adapters/cursor.js +139 -0
  33. package/dist/routes/adapters/index.d.ts +16 -0
  34. package/dist/routes/adapters/index.js +56 -0
  35. package/dist/routes/adapters/router.d.ts +31 -0
  36. package/dist/routes/adapters/router.js +97 -0
  37. package/dist/routes/adapters/schema.d.ts +141 -0
  38. package/dist/routes/adapters/schema.js +83 -0
  39. package/dist/routes/adapters/windsurf.d.ts +6 -0
  40. package/dist/routes/adapters/windsurf.js +48 -0
  41. package/dist/routes/admin.d.ts +15 -0
  42. package/dist/routes/admin.js +399 -0
  43. package/dist/routes/call.d.ts +13 -0
  44. package/dist/routes/call.js +68 -0
  45. package/dist/routes/events.d.ts +7 -0
  46. package/dist/routes/events.js +125 -0
  47. package/dist/routes/health.d.ts +2 -0
  48. package/dist/routes/health.js +12 -0
  49. package/dist/routes/hooks.d.ts +11 -0
  50. package/dist/routes/hooks.js +166 -0
  51. package/dist/routes/mcp.d.ts +10 -0
  52. package/dist/routes/mcp.js +170 -0
  53. package/dist/routes/openai-tools.d.ts +9 -0
  54. package/dist/routes/openai-tools.js +121 -0
  55. package/dist/server.d.ts +11 -0
  56. package/dist/server.js +118 -0
  57. package/dist/services/audit.d.ts +92 -0
  58. package/dist/services/audit.js +388 -0
  59. package/dist/services/data-dir.d.ts +7 -0
  60. package/dist/services/data-dir.js +61 -0
  61. package/dist/services/local-policy-templates.d.ts +9 -0
  62. package/dist/services/local-policy-templates.js +47 -0
  63. package/dist/services/local-policy.d.ts +39 -0
  64. package/dist/services/local-policy.js +172 -0
  65. package/dist/services/policy-store.d.ts +82 -0
  66. package/dist/services/policy-store.js +331 -0
  67. package/dist/services/policy.d.ts +8 -0
  68. package/dist/services/policy.js +126 -0
  69. package/dist/services/ratelimit.d.ts +26 -0
  70. package/dist/services/ratelimit.js +60 -0
  71. package/dist/services/sanitizer.d.ts +9 -0
  72. package/dist/services/sanitizer.js +73 -0
  73. package/dist/services/sqlite-loader.d.ts +4 -0
  74. package/dist/services/sqlite-loader.js +16 -0
  75. package/dist/services/telemetry-log.d.ts +76 -0
  76. package/dist/services/telemetry-log.js +260 -0
  77. package/dist/services/tool-executor.d.ts +46 -0
  78. package/dist/services/tool-executor.js +167 -0
  79. package/dist/services/upstream.d.ts +18 -0
  80. package/dist/services/upstream.js +72 -0
  81. package/dist/types.d.ts +112 -0
  82. package/dist/types.js +3 -0
  83. package/package.json +72 -0
  84. package/public/favicon.svg +4 -0
  85. package/public/index.html +3893 -0
@@ -0,0 +1,3893 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Oculi Monitor — Dashboard</title>
7
+ <link rel="icon" type="image/svg+xml" href="/admin/favicon.svg" />
8
+ <link rel="preconnect" href="https://fonts.googleapis.com" />
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.7/dist/chart.umd.min.js"></script>
12
+ <style>
13
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
14
+
15
+ :root {
16
+ --bg-body: #0a0a0f;
17
+ --bg-secondary: #0f1117;
18
+ --bg-sidebar: #0f1117;
19
+ --bg-card: #14161e;
20
+ --bg-elevated: #1a1f2e;
21
+ --border: #1e2330;
22
+ --border-light: #2d3748;
23
+ --text-primary: #e2e8f0;
24
+ --text-secondary: #a0aec0;
25
+ --text-muted: #718096;
26
+ --accent-blue: #63b3ed;
27
+ --accent-blue-bright: #4da3e0;
28
+ --accent-green: #48bb78;
29
+ --accent-red: #fc8181;
30
+ --accent-teal: #81e6d9;
31
+ --accent-purple: #b794f4;
32
+ --accent-yellow: #f6e05e;
33
+ --sidebar-width: 220px;
34
+ --topbar-height: 56px;
35
+ }
36
+
37
+ body {
38
+ font-family: 'Inter', system-ui, -apple-system, sans-serif;
39
+ background: var(--bg-body);
40
+ color: var(--text-primary);
41
+ min-height: 100vh;
42
+ display: flex;
43
+ }
44
+
45
+ code, pre, .mono, .kql-input, .search-result-key, .search-result-val,
46
+ .stat-value, .policy-code, .test-input textarea, .test-output pre {
47
+ font-family: 'JetBrains Mono', monospace;
48
+ }
49
+
50
+ /* ── Sidebar ── */
51
+ .sidebar {
52
+ width: var(--sidebar-width);
53
+ min-height: 100vh;
54
+ background: var(--bg-sidebar);
55
+ border-right: 1px solid var(--border);
56
+ display: flex;
57
+ flex-direction: column;
58
+ position: fixed;
59
+ top: 0;
60
+ left: 0;
61
+ z-index: 100;
62
+ overflow: hidden;
63
+ transition: width 0.2s ease;
64
+ }
65
+
66
+ .sidebar.collapsed { width: 52px; }
67
+ .sidebar.collapsed .sidebar-brand-text { display: none; }
68
+ .sidebar.collapsed .nav-label { display: none; }
69
+ .sidebar.collapsed .sidebar-nav li a { justify-content: center; padding: 10px; }
70
+ .sidebar.collapsed .sidebar-brand { justify-content: center; padding: 20px 0 24px; }
71
+ .sidebar.collapsed .sidebar-collapse-icon { transform: rotate(180deg); }
72
+
73
+ /* Sidebar tooltips — only visible when sidebar is collapsed */
74
+ .sidebar.collapsed .sidebar-nav li { position: relative; }
75
+ .sidebar.collapsed .sidebar-nav li a::after {
76
+ content: attr(data-tooltip);
77
+ position: absolute;
78
+ left: calc(100% + 10px);
79
+ top: 50%;
80
+ transform: translateY(-50%);
81
+ background: var(--bg-card);
82
+ color: var(--text-primary);
83
+ padding: 5px 11px;
84
+ border-radius: 6px;
85
+ font-size: 0.78rem;
86
+ font-weight: 500;
87
+ white-space: nowrap;
88
+ opacity: 0;
89
+ pointer-events: none;
90
+ transition: opacity 0.12s;
91
+ border: 1px solid var(--border);
92
+ z-index: 300;
93
+ box-shadow: 0 4px 14px rgba(0,0,0,0.35);
94
+ }
95
+ .sidebar.collapsed .sidebar-nav li a:hover::after { opacity: 1; }
96
+
97
+ .sidebar-brand {
98
+ padding: 20px 18px 24px;
99
+ display: flex;
100
+ align-items: flex-start;
101
+ gap: 10px;
102
+ flex-shrink: 0;
103
+ }
104
+
105
+ .sidebar-brand-icon {
106
+ width: 24px;
107
+ height: 24px;
108
+ border: 2px solid var(--accent-blue);
109
+ border-radius: 50%;
110
+ margin-top: 2px;
111
+ flex-shrink: 0;
112
+ background: rgba(99, 179, 237, 0.1);
113
+ }
114
+
115
+ .sidebar-brand-text {
116
+ font-size: 1.05rem;
117
+ font-weight: 700;
118
+ color: var(--text-primary);
119
+ line-height: 1.35;
120
+ }
121
+
122
+ .sidebar-nav {
123
+ list-style: none;
124
+ display: flex;
125
+ flex-direction: column;
126
+ gap: 2px;
127
+ padding: 0 8px;
128
+ flex: 1;
129
+ }
130
+
131
+ .sidebar-nav li a {
132
+ display: flex;
133
+ align-items: center;
134
+ gap: 12px;
135
+ padding: 10px 12px;
136
+ border-radius: 8px;
137
+ color: var(--text-secondary);
138
+ text-decoration: none;
139
+ font-size: 0.88rem;
140
+ font-weight: 500;
141
+ transition: background 0.15s, color 0.15s;
142
+ white-space: nowrap;
143
+ }
144
+
145
+ .sidebar-nav li a:hover {
146
+ background: rgba(255,255,255,0.05);
147
+ color: var(--text-primary);
148
+ }
149
+
150
+ .sidebar-nav li a.active {
151
+ background: rgba(99, 179, 237, 0.12);
152
+ color: var(--text-primary);
153
+ }
154
+
155
+ .sidebar-nav li a svg {
156
+ width: 18px;
157
+ height: 18px;
158
+ flex-shrink: 0;
159
+ stroke: currentColor;
160
+ fill: none;
161
+ stroke-width: 1.8;
162
+ stroke-linecap: round;
163
+ stroke-linejoin: round;
164
+ }
165
+
166
+ .sidebar-collapse-btn {
167
+ display: flex; align-items: center; gap: 12px;
168
+ padding: 14px 20px; color: var(--text-muted); font-size: 0.83rem;
169
+ font-weight: 500; cursor: pointer; border-top: 1px solid var(--border);
170
+ flex-shrink: 0; white-space: nowrap;
171
+ transition: color 0.15s; user-select: none;
172
+ }
173
+ .sidebar-collapse-btn:hover { color: var(--text-primary); }
174
+ .sidebar-collapse-icon {
175
+ width: 16px; height: 16px; flex-shrink: 0;
176
+ stroke: currentColor; fill: none; stroke-width: 2;
177
+ stroke-linecap: round; stroke-linejoin: round;
178
+ transition: transform 0.2s ease;
179
+ }
180
+ .sidebar.collapsed .sidebar-collapse-btn { justify-content: center; padding: 14px 0; }
181
+
182
+ /* ── Main wrapper ── */
183
+ .main-wrapper {
184
+ margin-left: var(--sidebar-width);
185
+ flex: 1;
186
+ display: flex;
187
+ flex-direction: column;
188
+ min-height: 100vh;
189
+ transition: margin-left 0.2s ease;
190
+ }
191
+ .main-wrapper.collapsed { margin-left: 52px; }
192
+
193
+ /* ── Top bar ── */
194
+ .topbar {
195
+ height: var(--topbar-height);
196
+ background: var(--bg-sidebar);
197
+ border-bottom: 1px solid var(--border);
198
+ display: flex;
199
+ align-items: center;
200
+ padding: 0 24px;
201
+ gap: 16px;
202
+ position: sticky;
203
+ top: 0;
204
+ z-index: 50;
205
+ }
206
+
207
+ .topbar-spacer { flex: 1; }
208
+
209
+
210
+ .topbar-icon {
211
+ width: 36px;
212
+ height: 36px;
213
+ display: flex;
214
+ align-items: center;
215
+ justify-content: center;
216
+ border-radius: 8px;
217
+ cursor: pointer;
218
+ transition: background 0.15s;
219
+ }
220
+
221
+ .topbar-icon:hover { background: rgba(255,255,255,0.06); }
222
+
223
+ .topbar-icon svg {
224
+ width: 20px;
225
+ height: 20px;
226
+ stroke: var(--text-secondary);
227
+ fill: none;
228
+ stroke-width: 1.8;
229
+ }
230
+
231
+ /* ── Content ── */
232
+ .content {
233
+ padding: 28px 32px 48px;
234
+ flex: 1;
235
+ }
236
+
237
+ .page-title {
238
+ font-size: 1.45rem;
239
+ font-weight: 700;
240
+ margin-bottom: 4px;
241
+ }
242
+
243
+ .page-subtitle {
244
+ font-size: 0.9rem;
245
+ color: var(--text-muted);
246
+ margin-bottom: 28px;
247
+ }
248
+
249
+ /* ── Stat cards ── */
250
+ .stat-cards {
251
+ display: grid;
252
+ grid-template-columns: repeat(4, 1fr);
253
+ gap: 16px;
254
+ margin-bottom: 24px;
255
+ }
256
+
257
+ .stat-card {
258
+ background: var(--bg-card);
259
+ border: 1px solid var(--border);
260
+ border-radius: 12px;
261
+ padding: 20px 22px;
262
+ }
263
+
264
+ .stat-card-label {
265
+ font-size: 0.78rem;
266
+ color: var(--text-muted);
267
+ margin-bottom: 8px;
268
+ }
269
+
270
+ .stat-card-value {
271
+ font-size: 1.85rem;
272
+ font-weight: 700;
273
+ color: var(--text-primary);
274
+ margin-bottom: 6px;
275
+ }
276
+
277
+ .stat-card-change {
278
+ font-size: 0.78rem;
279
+ font-weight: 600;
280
+ }
281
+
282
+ .stat-card-change.up-good { color: var(--accent-green); }
283
+ .stat-card-change.up-bad { color: var(--accent-red); }
284
+ .stat-card-change.down-good { color: var(--accent-green); }
285
+
286
+ /* ── Chart row ── */
287
+ .chart-row {
288
+ display: grid;
289
+ grid-template-columns: 2fr 1fr;
290
+ gap: 16px;
291
+ margin-bottom: 24px;
292
+ }
293
+
294
+ .card {
295
+ background: var(--bg-card);
296
+ border: 1px solid var(--border);
297
+ border-radius: 12px;
298
+ padding: 22px 24px;
299
+ }
300
+
301
+ .card-title {
302
+ font-size: 0.95rem;
303
+ font-weight: 600;
304
+ margin-bottom: 18px;
305
+ }
306
+
307
+ .chart-container {
308
+ position: relative;
309
+ width: 100%;
310
+ }
311
+
312
+ .chart-container.line-chart { height: 240px; }
313
+ .chart-container.pie-chart {
314
+ height: 240px;
315
+ display: flex;
316
+ align-items: center;
317
+ justify-content: center;
318
+ }
319
+
320
+ /* ── Bottom lists ── */
321
+ .lists-row {
322
+ display: grid;
323
+ grid-template-columns: repeat(3, 1fr);
324
+ gap: 16px;
325
+ }
326
+
327
+ .list-item {
328
+ display: flex;
329
+ align-items: center;
330
+ justify-content: space-between;
331
+ padding: 10px 0;
332
+ border-bottom: 1px solid rgba(45, 55, 72, 0.5);
333
+ }
334
+
335
+ .list-item:last-child { border-bottom: none; }
336
+
337
+ .list-item-left {
338
+ display: flex;
339
+ align-items: center;
340
+ gap: 8px;
341
+ font-size: 0.85rem;
342
+ }
343
+
344
+ .list-item-rank {
345
+ color: var(--text-muted);
346
+ font-size: 0.8rem;
347
+ min-width: 22px;
348
+ }
349
+
350
+ .list-item-name {
351
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
352
+ font-size: 0.82rem;
353
+ color: var(--text-secondary);
354
+ }
355
+
356
+ .list-item-value {
357
+ font-weight: 600;
358
+ font-size: 0.88rem;
359
+ font-variant-numeric: tabular-nums;
360
+ }
361
+
362
+ .list-item-value.blue { color: var(--accent-blue); }
363
+ .list-item-value.red { color: var(--accent-red); }
364
+ .list-item-value.green { color: var(--accent-green); }
365
+
366
+ /* ── Settings page ── */
367
+ .page { display: none; }
368
+ .page.active { display: block; }
369
+
370
+ /* Toggle switch — used by Policies "Enabled" column in the rules table */
371
+ .st-switch { position: relative; display: inline-block; width: 46px; height: 26px; flex-shrink: 0; }
372
+ .st-switch input { opacity: 0; width: 0; height: 0; position: absolute; }
373
+ .st-slider {
374
+ position: absolute; inset: 0; cursor: pointer;
375
+ background: #3a4256; border-radius: 26px; transition: background 0.2s;
376
+ }
377
+ .st-slider:before {
378
+ content: ''; position: absolute;
379
+ width: 20px; height: 20px; border-radius: 50%;
380
+ left: 3px; top: 3px; background: white;
381
+ transition: transform 0.2s; box-shadow: 0 1px 3px rgba(0,0,0,0.3);
382
+ }
383
+ .st-switch input:checked + .st-slider { background: var(--accent-green); }
384
+ .st-switch input:checked + .st-slider:before { transform: translateX(20px); }
385
+
386
+ .upstream-card-id {
387
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
388
+ font-size: 0.78rem;
389
+ color: var(--text-muted);
390
+ }
391
+
392
+ .upstream-card-details {
393
+ display: grid;
394
+ grid-template-columns: 1fr 1fr;
395
+ gap: 8px;
396
+ }
397
+
398
+ .upstream-card-details .setting-label { font-size: 0.72rem; }
399
+ .upstream-card-details .setting-value { padding: 5px 10px; font-size: 0.82rem; }
400
+
401
+ .code-block {
402
+ background: var(--bg-body);
403
+ border: 1px solid var(--border);
404
+ border-radius: 8px;
405
+ padding: 16px 18px;
406
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
407
+ font-size: 0.82rem;
408
+ color: var(--text-secondary);
409
+ white-space: pre;
410
+ overflow-x: auto;
411
+ line-height: 1.6;
412
+ }
413
+
414
+ .code-block-header {
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: space-between;
418
+ margin-bottom: 8px;
419
+ }
420
+
421
+ .code-block-label {
422
+ font-size: 0.78rem;
423
+ color: var(--text-muted);
424
+ }
425
+
426
+ .copy-btn {
427
+ background: var(--bg-card);
428
+ border: 1px solid var(--border);
429
+ color: var(--text-secondary);
430
+ font-size: 0.75rem;
431
+ padding: 3px 10px;
432
+ border-radius: 5px;
433
+ cursor: pointer;
434
+ transition: background 0.15s, color 0.15s;
435
+ }
436
+
437
+ .copy-btn:hover {
438
+ background: rgba(255,255,255,0.08);
439
+ color: var(--text-primary);
440
+ }
441
+
442
+ /* ── Visual Rules ── */
443
+ .rules-tab-bar {
444
+ display: flex;
445
+ gap: 0;
446
+ padding: 16px 24px 0;
447
+ border-bottom: 1px solid var(--border);
448
+ background: var(--bg-secondary);
449
+ }
450
+ .rules-tab {
451
+ background: none;
452
+ border: none;
453
+ color: var(--text-muted);
454
+ font-size: 0.88rem;
455
+ font-weight: 600;
456
+ padding: 10px 20px;
457
+ cursor: pointer;
458
+ border-bottom: 2px solid transparent;
459
+ transition: color 0.15s, border-color 0.15s;
460
+ }
461
+ .rules-tab:hover { color: var(--text-primary); }
462
+ .rules-tab.active {
463
+ color: var(--accent-blue);
464
+ border-bottom-color: var(--accent-blue);
465
+ }
466
+ .rules-header {
467
+ display: flex;
468
+ align-items: flex-start;
469
+ justify-content: space-between;
470
+ padding: 24px 24px 16px;
471
+ }
472
+ .rules-header-title { font-size: 1.1rem; font-weight: 700; }
473
+ .rules-header-subtitle { font-size: 0.82rem; color: var(--text-muted); margin-top: 2px; }
474
+ .rules-table-wrap {
475
+ padding: 0 24px 24px;
476
+ }
477
+ .rules-table {
478
+ width: 100%;
479
+ border-collapse: collapse;
480
+ background: var(--bg-card);
481
+ border: 1px solid var(--border);
482
+ border-radius: 10px;
483
+ overflow: hidden;
484
+ }
485
+ .rules-table th {
486
+ text-align: left;
487
+ font-size: 0.72rem;
488
+ font-weight: 600;
489
+ color: var(--text-muted);
490
+ text-transform: uppercase;
491
+ letter-spacing: 0.04em;
492
+ padding: 12px 16px;
493
+ background: var(--bg-elevated);
494
+ border-bottom: 1px solid var(--border);
495
+ }
496
+ .rules-table td {
497
+ padding: 12px 16px;
498
+ font-size: 0.85rem;
499
+ color: var(--text-primary);
500
+ border-bottom: 1px solid var(--border);
501
+ }
502
+ .rules-table tr:last-child td { border-bottom: none; }
503
+ .rules-table tr:hover td { background: rgba(255,255,255,0.02); }
504
+ .rule-id-cell {
505
+ font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
506
+ font-size: 0.82rem;
507
+ font-weight: 600;
508
+ }
509
+ .rule-pattern-cell {
510
+ font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
511
+ font-size: 0.78rem;
512
+ color: var(--text-secondary);
513
+ }
514
+ .rule-action-badge {
515
+ display: inline-block;
516
+ padding: 3px 10px;
517
+ border-radius: 20px;
518
+ font-size: 0.72rem;
519
+ font-weight: 600;
520
+ text-transform: uppercase;
521
+ letter-spacing: 0.03em;
522
+ }
523
+ .rule-action-badge.deny {
524
+ background: rgba(252,129,129,0.15);
525
+ color: var(--accent-red);
526
+ border: 1px solid rgba(252,129,129,0.3);
527
+ }
528
+ .rule-action-badge.warn {
529
+ background: rgba(246,224,94,0.15);
530
+ color: var(--accent-yellow);
531
+ border: 1px solid rgba(246,224,94,0.3);
532
+ }
533
+ .rule-action-badge.allow {
534
+ background: rgba(72,187,120,0.15);
535
+ color: var(--accent-green);
536
+ border: 1px solid rgba(72,187,120,0.3);
537
+ }
538
+ .rule-actions-cell {
539
+ display: flex;
540
+ align-items: center;
541
+ gap: 6px;
542
+ }
543
+ .rule-action-btn {
544
+ background: none;
545
+ border: none;
546
+ color: var(--text-muted);
547
+ cursor: pointer;
548
+ padding: 4px 6px;
549
+ border-radius: 4px;
550
+ transition: color 0.15s, background 0.15s;
551
+ }
552
+ .rule-action-btn:hover {
553
+ color: var(--text-primary);
554
+ background: rgba(255,255,255,0.06);
555
+ }
556
+ .rule-action-btn.delete:hover {
557
+ color: var(--accent-red);
558
+ background: rgba(252,129,129,0.1);
559
+ }
560
+ .rule-desc-cell {
561
+ font-size: 0.78rem;
562
+ color: var(--text-muted);
563
+ max-width: 200px;
564
+ overflow: hidden;
565
+ text-overflow: ellipsis;
566
+ white-space: nowrap;
567
+ }
568
+ .rule-form-group {
569
+ margin-bottom: 14px;
570
+ }
571
+ .rule-form-label {
572
+ font-size: 0.72rem;
573
+ font-weight: 600;
574
+ color: var(--text-muted);
575
+ text-transform: uppercase;
576
+ letter-spacing: 0.04em;
577
+ margin-bottom: 6px;
578
+ display: block;
579
+ }
580
+ .rule-form-input {
581
+ width: 100%;
582
+ background: var(--bg-body);
583
+ border: 1px solid var(--border);
584
+ border-radius: 8px;
585
+ color: var(--text-primary);
586
+ font-size: 0.85rem;
587
+ padding: 9px 12px;
588
+ outline: none;
589
+ transition: border-color 0.15s;
590
+ box-sizing: border-box;
591
+ }
592
+ .rule-form-input:focus { border-color: rgba(99,179,237,0.5); }
593
+ .rule-form-input.mono {
594
+ font-family: 'JetBrains Mono', 'SF Mono', 'Fira Code', monospace;
595
+ font-size: 0.82rem;
596
+ }
597
+ .rule-form-select {
598
+ width: 100%;
599
+ background: var(--bg-body);
600
+ border: 1px solid var(--border);
601
+ border-radius: 8px;
602
+ color: var(--text-primary);
603
+ font-size: 0.85rem;
604
+ padding: 9px 12px;
605
+ outline: none;
606
+ appearance: none;
607
+ cursor: pointer;
608
+ box-sizing: border-box;
609
+ background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' fill='%23718096'%3E%3Cpath d='M2 4l4 4 4-4'/%3E%3C/svg%3E");
610
+ background-repeat: no-repeat;
611
+ background-position: right 12px center;
612
+ }
613
+ .rule-form-select option { background: #1a1f2e; }
614
+ .rules-empty {
615
+ text-align: center;
616
+ padding: 40px 20px;
617
+ color: var(--text-muted);
618
+ font-size: 0.88rem;
619
+ }
620
+
621
+ /* ── Policies page ── */
622
+ .policies-layout {
623
+ display: grid;
624
+ grid-template-columns: 35% 1fr;
625
+ gap: 0;
626
+ min-height: calc(100vh - var(--topbar-height) - 80px);
627
+ }
628
+
629
+ .policies-left {
630
+ border-right: 1px solid var(--border);
631
+ padding: 24px 20px;
632
+ overflow-y: auto;
633
+ }
634
+
635
+ .policies-left-header {
636
+ display: flex;
637
+ align-items: flex-start;
638
+ justify-content: space-between;
639
+ margin-bottom: 20px;
640
+ }
641
+
642
+ .policies-left-title {
643
+ font-size: 1.1rem;
644
+ font-weight: 700;
645
+ }
646
+
647
+ .policies-left-subtitle {
648
+ font-size: 0.82rem;
649
+ color: var(--text-muted);
650
+ margin-top: 2px;
651
+ }
652
+
653
+ .btn-new-policy {
654
+ background: var(--accent-green);
655
+ color: #fff;
656
+ border: none;
657
+ border-radius: 8px;
658
+ padding: 8px 16px;
659
+ font-size: 0.82rem;
660
+ font-weight: 600;
661
+ cursor: pointer;
662
+ display: flex;
663
+ align-items: center;
664
+ gap: 6px;
665
+ transition: opacity 0.15s;
666
+ white-space: nowrap;
667
+ }
668
+
669
+ .btn-new-policy:hover { opacity: 0.85; }
670
+
671
+ .policy-cards {
672
+ display: flex;
673
+ flex-direction: column;
674
+ gap: 10px;
675
+ }
676
+
677
+ .policy-card {
678
+ background: var(--bg-card);
679
+ border: 1px solid var(--border);
680
+ border-radius: 10px;
681
+ padding: 16px 18px;
682
+ cursor: pointer;
683
+ transition: border-color 0.15s, background 0.15s;
684
+ }
685
+
686
+ .policy-card:hover { border-color: rgba(99, 179, 237, 0.4); }
687
+
688
+ .policy-card.selected {
689
+ border-color: var(--accent-blue);
690
+ background: rgba(99, 179, 237, 0.08);
691
+ }
692
+
693
+ .policy-card-name {
694
+ font-weight: 600;
695
+ font-size: 0.92rem;
696
+ margin-bottom: 6px;
697
+ }
698
+
699
+ .policy-card-meta {
700
+ display: flex;
701
+ align-items: center;
702
+ gap: 8px;
703
+ flex-wrap: wrap;
704
+ }
705
+
706
+ .policy-version-badge {
707
+ background: rgba(72, 187, 120, 0.15);
708
+ color: var(--accent-green);
709
+ border: 1px solid rgba(72, 187, 120, 0.3);
710
+ border-radius: 20px;
711
+ padding: 2px 10px;
712
+ font-size: 0.72rem;
713
+ font-weight: 600;
714
+ font-family: 'SF Mono', 'Fira Code', monospace;
715
+ }
716
+
717
+ .policy-card-updated {
718
+ font-size: 0.75rem;
719
+ color: var(--text-muted);
720
+ }
721
+
722
+ .policies-right {
723
+ padding: 24px 28px;
724
+ overflow-y: auto;
725
+ }
726
+
727
+ .policies-right-empty {
728
+ display: flex;
729
+ align-items: center;
730
+ justify-content: center;
731
+ height: 300px;
732
+ color: var(--text-muted);
733
+ font-size: 0.9rem;
734
+ }
735
+
736
+ .policy-detail-header {
737
+ display: flex;
738
+ align-items: center;
739
+ justify-content: space-between;
740
+ margin-bottom: 20px;
741
+ }
742
+
743
+ .policy-detail-name {
744
+ font-size: 1.25rem;
745
+ font-weight: 700;
746
+ }
747
+
748
+ .policy-detail-actions {
749
+ display: flex;
750
+ align-items: center;
751
+ gap: 8px;
752
+ }
753
+
754
+ .btn-policy-action {
755
+ background: var(--bg-card);
756
+ border: 1px solid var(--border);
757
+ color: var(--text-secondary);
758
+ border-radius: 8px;
759
+ padding: 7px 14px;
760
+ font-size: 0.8rem;
761
+ font-weight: 500;
762
+ cursor: pointer;
763
+ display: flex;
764
+ align-items: center;
765
+ gap: 6px;
766
+ transition: background 0.15s, color 0.15s;
767
+ }
768
+
769
+ .btn-policy-action:hover {
770
+ background: rgba(255, 255, 255, 0.06);
771
+ color: var(--text-primary);
772
+ }
773
+
774
+ .btn-policy-action svg {
775
+ width: 14px;
776
+ height: 14px;
777
+ stroke: currentColor;
778
+ fill: none;
779
+ stroke-width: 2;
780
+ stroke-linecap: round;
781
+ stroke-linejoin: round;
782
+ }
783
+
784
+ .btn-save-publish {
785
+ background: var(--accent-green);
786
+ color: #fff;
787
+ border: none;
788
+ border-radius: 8px;
789
+ padding: 8px 18px;
790
+ font-size: 0.82rem;
791
+ font-weight: 600;
792
+ cursor: pointer;
793
+ transition: opacity 0.15s;
794
+ }
795
+
796
+ .btn-save-publish:hover { opacity: 0.85; }
797
+
798
+ .policy-editor-area {
799
+ background: var(--bg-body);
800
+ border: 1px solid var(--border);
801
+ border-radius: 10px;
802
+ margin-bottom: 24px;
803
+ }
804
+
805
+ .policy-rego-textarea {
806
+ width: 100%;
807
+ min-height: 320px;
808
+ background: transparent;
809
+ border: none;
810
+ color: var(--text-secondary);
811
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
812
+ font-size: 0.82rem;
813
+ line-height: 1.65;
814
+ padding: 18px 20px;
815
+ resize: vertical;
816
+ outline: none;
817
+ tab-size: 4;
818
+ }
819
+
820
+ .policy-test-section {
821
+ background: var(--bg-card);
822
+ border: 1px solid var(--border);
823
+ border-radius: 12px;
824
+ padding: 22px 24px;
825
+ }
826
+
827
+ .policy-test-title {
828
+ font-size: 0.95rem;
829
+ font-weight: 600;
830
+ margin-bottom: 16px;
831
+ }
832
+
833
+ .policy-test-grid {
834
+ display: grid;
835
+ grid-template-columns: 1fr 1fr;
836
+ gap: 16px;
837
+ margin-bottom: 16px;
838
+ }
839
+
840
+ .policy-test-label {
841
+ font-size: 0.78rem;
842
+ color: var(--text-muted);
843
+ text-transform: uppercase;
844
+ letter-spacing: 0.04em;
845
+ margin-bottom: 6px;
846
+ }
847
+
848
+ .policy-test-input {
849
+ width: 100%;
850
+ min-height: 160px;
851
+ background: var(--bg-body);
852
+ border: 1px solid var(--border);
853
+ border-radius: 8px;
854
+ color: var(--text-secondary);
855
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
856
+ font-size: 0.8rem;
857
+ line-height: 1.5;
858
+ padding: 12px 14px;
859
+ resize: vertical;
860
+ outline: none;
861
+ }
862
+
863
+ .policy-test-output {
864
+ width: 100%;
865
+ min-height: 160px;
866
+ background: var(--bg-body);
867
+ border: 1px solid var(--border);
868
+ border-radius: 8px;
869
+ color: var(--text-muted);
870
+ font-family: 'SF Mono', 'Fira Code', 'Cascadia Code', monospace;
871
+ font-size: 0.8rem;
872
+ line-height: 1.5;
873
+ padding: 12px 14px;
874
+ overflow-y: auto;
875
+ white-space: pre-wrap;
876
+ }
877
+
878
+ .policy-test-buttons {
879
+ display: flex;
880
+ gap: 10px;
881
+ }
882
+
883
+ .btn-run-test {
884
+ background: var(--accent-green);
885
+ color: #fff;
886
+ border: none;
887
+ border-radius: 8px;
888
+ padding: 8px 20px;
889
+ font-size: 0.82rem;
890
+ font-weight: 600;
891
+ cursor: pointer;
892
+ display: flex;
893
+ align-items: center;
894
+ gap: 6px;
895
+ transition: opacity 0.15s;
896
+ }
897
+
898
+ .btn-run-test:hover { opacity: 0.85; }
899
+
900
+ .btn-load-sample {
901
+ background: transparent;
902
+ border: 1px solid var(--border);
903
+ color: var(--text-secondary);
904
+ border-radius: 8px;
905
+ padding: 8px 18px;
906
+ font-size: 0.82rem;
907
+ font-weight: 500;
908
+ cursor: pointer;
909
+ transition: background 0.15s, color 0.15s;
910
+ }
911
+
912
+ .btn-load-sample:hover {
913
+ background: rgba(255, 255, 255, 0.05);
914
+ color: var(--text-primary);
915
+ }
916
+
917
+ /* ── Search Page (KQL) ── */
918
+ @keyframes pulse-dot { 0%, 100% { opacity: 1; } 50% { opacity: 0.4; } }
919
+ /* KQL bar */
920
+ .kql-topbar { display: flex; gap: 10px; align-items: center; margin-bottom: 10px; }
921
+ .kql-bar {
922
+ flex: 1; display: flex; align-items: center;
923
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; overflow: hidden;
924
+ }
925
+ .kql-input {
926
+ flex: 1; background: none; border: none; color: var(--text-primary);
927
+ font-family: 'Courier New', monospace; font-size: 0.85rem;
928
+ padding: 11px 14px; outline: none;
929
+ }
930
+ .kql-input::placeholder { color: var(--text-muted); }
931
+ .btn-kql-search {
932
+ background: var(--accent-green); color: #0f1117; border: none;
933
+ padding: 11px 20px; font-size: 0.85rem; font-weight: 700; cursor: pointer;
934
+ display: flex; align-items: center; gap: 7px; white-space: nowrap; flex-shrink: 0;
935
+ }
936
+ .btn-kql-search:hover { opacity: 0.9; }
937
+ .btn-kql-search svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 2.5; stroke-linecap: round; }
938
+ .time-picker-wrap { position: relative; flex-shrink: 0; }
939
+ .btn-time-picker {
940
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary);
941
+ padding: 10px 14px; border-radius: 8px; font-size: 0.82rem; font-weight: 500;
942
+ cursor: pointer; display: flex; align-items: center; gap: 8px; white-space: nowrap;
943
+ }
944
+ .btn-time-picker:hover { color: var(--text-primary); border-color: var(--accent-blue); }
945
+ .btn-time-picker.active { border-color: var(--accent-blue); color: var(--text-primary); }
946
+ .time-picker-dropdown {
947
+ display: none; position: absolute; top: calc(100% + 8px); right: 0;
948
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px;
949
+ padding: 16px; width: 340px; z-index: 200;
950
+ box-shadow: 0 12px 40px rgba(0,0,0,0.5);
951
+ }
952
+ .time-picker-dropdown.open { display: block; }
953
+ .tp-section-lbl { font-size: 0.7rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.07em; margin-bottom: 8px; }
954
+ .tp-presets { display: grid; grid-template-columns: 1fr 1fr; gap: 6px; }
955
+ .btn-tp-preset {
956
+ background: var(--bg-body); border: 1px solid var(--border); color: var(--text-secondary);
957
+ padding: 7px 10px; border-radius: 6px; font-size: 0.8rem; cursor: pointer; text-align: left;
958
+ transition: background 0.1s, color 0.1s, border-color 0.1s;
959
+ }
960
+ .btn-tp-preset:hover { background: rgba(99,179,237,0.08); border-color: var(--accent-blue); color: var(--text-primary); }
961
+ .btn-tp-preset.active { background: rgba(99,179,237,0.15); border-color: var(--accent-blue); color: var(--accent-blue); font-weight: 600; }
962
+ .tp-divider { border: none; border-top: 1px solid var(--border); margin: 14px 0; }
963
+ .tp-custom-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 10px; }
964
+ .tp-field-lbl { font-size: 0.72rem; color: var(--text-muted); margin-bottom: 4px; }
965
+ .tp-datetime-input {
966
+ width: 100%; background: var(--bg-body); border: 1px solid var(--border);
967
+ color: var(--text-primary); border-radius: 6px; padding: 7px 9px; font-size: 0.78rem;
968
+ outline: none; box-sizing: border-box;
969
+ }
970
+ .tp-datetime-input:focus { border-color: var(--accent-blue); }
971
+ .btn-tp-apply {
972
+ width: 100%; background: var(--accent-blue); color: #fff; border: none;
973
+ padding: 9px; border-radius: 6px; font-size: 0.83rem; font-weight: 600; cursor: pointer;
974
+ }
975
+ .btn-tp-apply:hover { opacity: 0.9; }
976
+ .btn-export-kql {
977
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary);
978
+ padding: 10px 14px; border-radius: 8px; font-size: 0.82rem; font-weight: 600;
979
+ cursor: pointer; display: flex; align-items: center; gap: 6px; white-space: nowrap; flex-shrink: 0;
980
+ }
981
+ .btn-export-kql:hover { color: var(--text-primary); }
982
+ /* Filter chips row */
983
+ .kql-filter-row { display: flex; align-items: center; gap: 8px; margin-bottom: 14px; flex-wrap: wrap; }
984
+ .filter-panel-wrap { position: relative; flex-shrink: 0; }
985
+ .btn-add-filter {
986
+ background: none; border: 1px solid var(--accent-blue); color: var(--accent-blue);
987
+ padding: 5px 12px; border-radius: 6px; font-size: 0.8rem; font-weight: 600;
988
+ cursor: pointer; display: flex; align-items: center; gap: 5px;
989
+ }
990
+ .btn-add-filter:hover { background: rgba(99,179,237,0.08); }
991
+ .filter-panel-dropdown {
992
+ display: none; position: absolute; top: calc(100% + 8px); left: 0;
993
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px;
994
+ padding: 18px; width: 300px; z-index: 200;
995
+ box-shadow: 0 12px 40px rgba(0,0,0,0.5);
996
+ }
997
+ .filter-panel-dropdown.open { display: block; }
998
+ .fp-section-lbl { font-size: 0.72rem; color: var(--text-muted); letter-spacing: 0.03em; margin-bottom: 8px; font-weight: 500; }
999
+ .fp-btn-row { display: flex; gap: 8px; }
1000
+ .btn-fp-toggle {
1001
+ flex: 1; background: var(--bg-body); border: 1px solid var(--border);
1002
+ color: var(--text-secondary); padding: 10px 8px; border-radius: 8px;
1003
+ font-size: 0.85rem; font-weight: 700; cursor: pointer; text-align: center;
1004
+ transition: background 0.1s, color 0.1s, border-color 0.1s;
1005
+ }
1006
+ .btn-fp-toggle:hover { border-color: rgba(99,179,237,0.5); color: var(--text-primary); }
1007
+ .btn-fp-toggle.active { background: rgba(99,179,237,0.12); border-color: var(--accent-blue); color: var(--text-primary); }
1008
+ .fp-spacer { margin-top: 14px; }
1009
+ .fp-text-input {
1010
+ width: 100%; background: var(--bg-body); border: 1px solid var(--border);
1011
+ color: var(--text-primary); border-radius: 8px; padding: 10px 12px;
1012
+ font-size: 0.85rem; box-sizing: border-box; outline: none; margin-top: 0;
1013
+ }
1014
+ .fp-text-input:focus { border-color: rgba(99,179,237,0.6); }
1015
+ .fp-text-input::placeholder { color: var(--text-muted); }
1016
+ .fp-hint { font-size: 0.72rem; color: var(--text-muted); margin-top: 4px; margin-bottom: 0; }
1017
+ .filter-chip {
1018
+ background: rgba(99,179,237,0.1); border: 1px solid rgba(99,179,237,0.3); color: var(--accent-blue);
1019
+ padding: 4px 10px; border-radius: 20px; font-size: 0.78rem; font-family: monospace;
1020
+ display: flex; align-items: center; gap: 6px;
1021
+ }
1022
+ .filter-chip-x { cursor: pointer; opacity: 0.7; font-style: normal; }
1023
+ .filter-chip-x:hover { opacity: 1; }
1024
+ .kql-result-meta { margin-left: auto; font-size: 0.8rem; color: var(--text-muted); flex-shrink: 0; }
1025
+ /* Tabs */
1026
+ .search-tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 0; }
1027
+ .search-tab {
1028
+ background: none; border: none; padding: 10px 20px; color: var(--text-muted);
1029
+ font-size: 0.84rem; font-weight: 500; cursor: pointer;
1030
+ border-bottom: 2px solid transparent; margin-bottom: -1px; transition: color 0.15s;
1031
+ }
1032
+ .search-tab:hover { color: var(--text-secondary); }
1033
+ .search-tab.active { color: var(--text-primary); border-bottom-color: var(--accent-blue); }
1034
+ /* Timeline */
1035
+ .search-timeline-wrap { padding: 14px 2px 6px; }
1036
+ .search-timeline-hdr { display: flex; justify-content: space-between; margin-bottom: 6px; }
1037
+ .search-timeline-lbl { font-size: 0.72rem; color: var(--text-muted); }
1038
+ .search-timeline-bar { display: flex; gap: 2px; align-items: flex-end; height: 36px; }
1039
+ .tl-bucket {
1040
+ flex: 1; border-radius: 2px; min-height: 3px; cursor: default;
1041
+ background: rgba(72,187,120,0.15); transition: opacity 0.15s;
1042
+ }
1043
+ .tl-bucket:hover { opacity: 0.75; }
1044
+ /* Log-style results table */
1045
+ .search-results-wrap {
1046
+ background: var(--bg-card); border: 1px solid var(--border);
1047
+ border-radius: 0 0 12px 12px; overflow: hidden;
1048
+ }
1049
+ .search-tabs-card { border-radius: 12px 12px 0 0; border: 1px solid var(--border); border-bottom: none; overflow: hidden; }
1050
+ .search-log-table { width: 100%; border-collapse: collapse; font-size: 0.82rem; }
1051
+ .search-log-table th {
1052
+ text-align: left; padding: 9px 16px; font-size: 0.73rem; font-weight: 600;
1053
+ color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em;
1054
+ border-bottom: 1px solid var(--border); background: rgba(0,0,0,0.2);
1055
+ }
1056
+ .search-log-table td { padding: 0; border-bottom: 1px solid rgba(45,55,72,0.4); vertical-align: top; }
1057
+ .search-log-table tr:last-child > td { border-bottom: none; }
1058
+ .slt-row {
1059
+ display: flex; align-items: flex-start; padding: 9px 16px; cursor: pointer;
1060
+ gap: 0; transition: background 0.1s;
1061
+ }
1062
+ .slt-row:hover { background: rgba(255,255,255,0.025); }
1063
+ .slt-chevron {
1064
+ color: var(--text-muted); font-size: 0.7rem; margin-right: 10px; flex-shrink: 0;
1065
+ transition: transform 0.15s; width: 12px; text-align: center; margin-top: 3px;
1066
+ }
1067
+ .slt-chevron.open { transform: rotate(90deg); color: var(--text-secondary); }
1068
+ .slt-time {
1069
+ color: var(--text-muted); font-size: 0.77rem; white-space: nowrap;
1070
+ margin-right: 20px; flex-shrink: 0; min-width: 130px; margin-top: 2px;
1071
+ }
1072
+ .slt-event { flex: 1; font-family: 'Courier New', monospace; font-size: 0.8rem; line-height: 1.6; word-break: break-word; }
1073
+ .kv-ts { color: #f6ad55; }
1074
+ .kv-key { color: var(--text-muted); }
1075
+ .kv-eq { color: var(--text-muted); }
1076
+ .kv-actor { color: var(--accent-green); }
1077
+ .kv-tool { color: var(--accent-green); }
1078
+ .kv-allow { color: var(--accent-green); font-weight: 700; }
1079
+ .kv-deny { color: var(--accent-red); font-weight: 700; }
1080
+ .kv-upstream { color: var(--accent-blue); }
1081
+ /* Inline expansion */
1082
+ .slt-expanded-td { padding: 0 !important; border-bottom: 1px solid var(--border) !important; }
1083
+ .slt-expansion {
1084
+ display: grid; grid-template-columns: 1fr 1fr; gap: 24px;
1085
+ padding: 20px 24px; background: rgba(10,12,18,0.7);
1086
+ border-top: 1px solid var(--border);
1087
+ }
1088
+ .slt-exp-left { display: flex; flex-direction: column; gap: 14px; }
1089
+ .slt-exp-right { display: flex; flex-direction: column; gap: 10px; }
1090
+ .slt-exp-field { display: flex; flex-direction: column; gap: 4px; }
1091
+ .slt-exp-lbl { font-size: 0.7rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
1092
+ .slt-exp-val { font-size: 0.88rem; color: var(--text-primary); }
1093
+ .slt-exp-code {
1094
+ background: var(--bg-body); border: 1px solid var(--border); border-radius: 6px;
1095
+ padding: 10px 12px; font-family: 'Courier New', monospace; font-size: 0.75rem;
1096
+ color: var(--text-secondary); white-space: pre-wrap; word-break: break-all;
1097
+ max-height: 150px; overflow-y: auto; line-height: 1.5;
1098
+ }
1099
+ /* Shared */
1100
+ .search-risk-score { font-weight: 600; font-size: 0.85rem; }
1101
+ .search-risk-low { color: var(--accent-green); }
1102
+ .search-risk-mid { color: #ecc94b; }
1103
+ .search-risk-high { color: var(--accent-red); }
1104
+ .search-empty { text-align: center; color: var(--text-muted); padding: 48px 24px; font-size: 0.88rem; }
1105
+ .search-no-results {
1106
+ display: flex; flex-direction: column; align-items: center; justify-content: center;
1107
+ padding: 60px 24px; gap: 10px; text-align: center;
1108
+ }
1109
+ .search-no-results-icon { font-size: 2rem; opacity: 0.35; margin-bottom: 4px; }
1110
+ .search-no-results-title { font-size: 0.95rem; font-weight: 600; color: var(--text-secondary); }
1111
+ .search-no-results-sub { font-size: 0.8rem; color: var(--text-muted); line-height: 1.6; max-width: 340px; }
1112
+ .search-no-results-tips { margin-top: 8px; text-align: left; font-size: 0.78rem; color: var(--text-muted); line-height: 1.8; }
1113
+ .search-no-results-tips li { list-style: disc; margin-left: 18px; }
1114
+ .search-pagination { display: flex; gap: 8px; justify-content: space-between; margin-top: 12px; align-items: center; }
1115
+ .search-pagination-left { display: flex; align-items: center; gap: 6px; color: var(--text-muted); font-size: 0.8rem; }
1116
+ .search-pagination-right { display: flex; gap: 8px; align-items: center; }
1117
+ .search-pagination button {
1118
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary);
1119
+ padding: 6px 12px; border-radius: 6px; font-size: 0.8rem; cursor: pointer;
1120
+ }
1121
+ .search-pagination button:hover:not(:disabled) { color: var(--text-primary); background: rgba(255,255,255,0.05); }
1122
+ .search-pagination button:disabled { opacity: 0.4; cursor: not-allowed; }
1123
+ .search-pagination .page-info { color: var(--text-muted); font-size: 0.8rem; }
1124
+ .search-page-size-select {
1125
+ background: var(--bg-card); border: 1px solid var(--border); color: var(--text-secondary);
1126
+ padding: 5px 8px; border-radius: 6px; font-size: 0.8rem; cursor: pointer; outline: none;
1127
+ }
1128
+ .search-page-size-select:focus { border-color: var(--accent-blue); }
1129
+ /* Tab content panels */
1130
+ .search-tab-panel { display: none; }
1131
+ .search-tab-panel.active { display: block; }
1132
+
1133
+ /* ── Search layout: fields sidebar + main ── */
1134
+ .search-layout { display: flex; gap: 14px; align-items: flex-start; }
1135
+ .search-fields-sidebar { width: 200px; flex-shrink: 0; }
1136
+ .search-main { flex: 1; min-width: 0; }
1137
+ .sfs-section { background: var(--bg-card); border: 1px solid var(--border); border-radius: 8px; margin-bottom: 10px; overflow: hidden; }
1138
+ .sfs-section-hdr {
1139
+ padding: 7px 11px; font-size: 0.66rem; font-weight: 700; color: var(--text-muted);
1140
+ text-transform: uppercase; letter-spacing: 0.08em; background: rgba(0,0,0,0.18);
1141
+ }
1142
+ .sfs-field { border-top: 1px solid rgba(45,55,72,0.4); }
1143
+ .sfs-field-hdr {
1144
+ display: flex; align-items: center; justify-content: space-between;
1145
+ padding: 5px 11px; cursor: pointer; gap: 6px; transition: background 0.1s;
1146
+ }
1147
+ .sfs-field-hdr:hover { background: rgba(255,255,255,0.03); }
1148
+ .sfs-field-name {
1149
+ font-size: 0.78rem; color: var(--accent-blue); font-family: 'Courier New', monospace;
1150
+ flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
1151
+ }
1152
+ .sfs-field-card {
1153
+ font-size: 0.68rem; color: var(--text-muted); flex-shrink: 0; background: rgba(45,55,72,0.5);
1154
+ padding: 1px 5px; border-radius: 3px;
1155
+ }
1156
+ .sfs-field-chevron { font-size: 0.55rem; color: var(--text-muted); flex-shrink: 0; transition: transform 0.15s; }
1157
+ .sfs-field-chevron.open { transform: rotate(90deg); }
1158
+ .sfs-field-values { display: none; padding: 2px 0 6px; }
1159
+ .sfs-field-values.open { display: block; }
1160
+ .sfs-value-row {
1161
+ display: flex; align-items: center; gap: 6px; padding: 3px 11px 3px 16px;
1162
+ cursor: pointer; transition: background 0.1s;
1163
+ }
1164
+ .sfs-value-row:hover { background: rgba(99,179,237,0.07); }
1165
+ .sfs-value-row:hover .sfs-value-label { color: var(--accent-blue); }
1166
+ .sfs-value-bar-wrap { width: 28px; height: 4px; background: rgba(45,55,72,0.6); border-radius: 2px; flex-shrink: 0; }
1167
+ .sfs-value-bar { height: 100%; background: var(--accent-blue); border-radius: 2px; opacity: 0.55; }
1168
+ .sfs-value-label {
1169
+ flex: 1; font-size: 0.72rem; color: var(--text-secondary); font-family: 'Courier New', monospace;
1170
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;
1171
+ }
1172
+ .sfs-value-count { font-size: 0.68rem; color: var(--text-muted); flex-shrink: 0; }
1173
+ /* Expansion: Splunk-style field table */
1174
+ .slt-expansion {
1175
+ display: grid; grid-template-columns: 1fr 1fr; gap: 20px;
1176
+ padding: 18px 22px; background: rgba(10,12,18,0.7); border-top: 1px solid var(--border);
1177
+ }
1178
+ .slt-exp-fields { display: flex; flex-direction: column; gap: 0; }
1179
+ .slt-exp-field-row {
1180
+ display: flex; align-items: baseline; gap: 8px; padding: 4px 0;
1181
+ border-bottom: 1px solid rgba(45,55,72,0.3); font-size: 0.8rem;
1182
+ }
1183
+ .slt-exp-field-row:last-child { border-bottom: none; }
1184
+ .slt-exp-fname {
1185
+ font-size: 0.72rem; color: var(--text-muted); font-family: 'Courier New', monospace;
1186
+ width: 92px; flex-shrink: 0; text-align: right; padding-right: 8px;
1187
+ }
1188
+ .slt-exp-fval { color: var(--text-primary); font-size: 0.82rem; word-break: break-word; }
1189
+ .slt-exp-fval.clickable { cursor: pointer; }
1190
+ .slt-exp-fval.clickable:hover { color: var(--accent-blue); text-decoration: underline; }
1191
+ .slt-exp-json-col { display: flex; flex-direction: column; gap: 12px; }
1192
+
1193
+ /* ── Call Details Panel ── */
1194
+ .call-details-overlay {
1195
+ position: fixed; top: 0; left: 0; right: 0; bottom: 0;
1196
+ background: rgba(0,0,0,0.45); z-index: 200; display: none;
1197
+ }
1198
+ .call-details-overlay.open { display: block; }
1199
+ .call-details-panel {
1200
+ position: fixed; top: 0; right: 0; bottom: 0; width: 380px; max-width: 90vw;
1201
+ background: var(--bg-card); border-left: 1px solid var(--border);
1202
+ z-index: 201; display: flex; flex-direction: column;
1203
+ transform: translateX(100%); transition: transform 0.2s ease; overflow: hidden;
1204
+ }
1205
+ .call-details-panel.open { transform: translateX(0); }
1206
+ .cdp-header {
1207
+ display: flex; justify-content: space-between; align-items: center;
1208
+ padding: 18px 20px; border-bottom: 1px solid var(--border); flex-shrink: 0;
1209
+ }
1210
+ .cdp-title { font-size: 1.05rem; font-weight: 700; color: var(--text-primary); }
1211
+ .cdp-close {
1212
+ background: none; border: none; color: var(--text-muted);
1213
+ cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 4px 8px; border-radius: 4px;
1214
+ }
1215
+ .cdp-close:hover { color: var(--text-primary); background: rgba(255,255,255,0.06); }
1216
+ .cdp-body { flex: 1; overflow-y: auto; padding: 20px; display: flex; flex-direction: column; gap: 14px; }
1217
+ .cdp-field { display: flex; flex-direction: column; gap: 4px; }
1218
+ .cdp-label { font-size: 0.72rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; }
1219
+ .cdp-value { font-size: 0.88rem; color: var(--text-primary); }
1220
+ .cdp-value-mono { font-family: 'Courier New', monospace; font-size: 0.82rem; color: var(--text-primary); }
1221
+ .cdp-value-muted { color: var(--text-muted); font-size: 0.82rem; }
1222
+ .cdp-row { display: grid; grid-template-columns: 1fr 1fr; gap: 14px; }
1223
+ .cdp-section-label { font-size: 0.72rem; font-weight: 600; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 6px; }
1224
+ .cdp-code {
1225
+ background: var(--bg-body); border: 1px solid var(--border); border-radius: 6px;
1226
+ padding: 10px 12px; font-family: 'Courier New', monospace; font-size: 0.76rem;
1227
+ color: var(--accent-teal); white-space: pre-wrap; word-break: break-all;
1228
+ max-height: 140px; overflow-y: auto; line-height: 1.5;
1229
+ }
1230
+ .cdp-divider { border: none; border-top: 1px solid var(--border); margin: 4px 0; }
1231
+ .search-table tbody tr { cursor: pointer; }
1232
+ .search-table tbody tr.selected td { background: rgba(99,179,237,0.07); }
1233
+
1234
+ /* ── Actors & Roles page ── */
1235
+ .actors-page-header { margin-bottom: 20px; }
1236
+ .actors-page-header h1 { font-size: 1.4rem; font-weight: 700; margin: 0 0 4px; }
1237
+ .actors-page-header p { font-size: 0.85rem; color: var(--text-muted); margin: 0; }
1238
+ .actors-table-card { background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
1239
+ .actors-table { width: 100%; border-collapse: collapse; font-size: 0.84rem; }
1240
+ .actors-table th {
1241
+ padding: 10px 16px; text-align: left; font-size: 0.72rem; color: var(--text-muted);
1242
+ text-transform: uppercase; letter-spacing: 0.06em; font-weight: 600;
1243
+ border-bottom: 1px solid var(--border); background: var(--bg-body);
1244
+ }
1245
+ .actors-table td { padding: 12px 16px; border-bottom: 1px solid rgba(45,55,72,0.4); vertical-align: middle; }
1246
+ .actors-table tbody tr { cursor: pointer; transition: background 0.1s; }
1247
+ .actors-table tbody tr:hover td { background: rgba(255,255,255,0.02); }
1248
+ .actors-table tbody tr.selected td { background: rgba(99,179,237,0.06); }
1249
+ .actors-table tbody tr:last-child td { border-bottom: none; }
1250
+ .actor-id-code { font-family: 'Courier New', monospace; font-size: 0.85rem; color: var(--text-primary); }
1251
+ .actor-type-badge {
1252
+ display: inline-block; padding: 2px 9px; border-radius: 4px; font-size: 0.75rem; font-weight: 600;
1253
+ background: rgba(255,255,255,0.07); border: 1px solid rgba(255,255,255,0.12); color: var(--text-secondary);
1254
+ }
1255
+ .role-badge {
1256
+ display: inline-block; padding: 2px 8px; border-radius: 4px; font-size: 0.75rem; font-weight: 600;
1257
+ margin-right: 4px; border: 1px solid;
1258
+ }
1259
+ .role-badge.role-admin { background: rgba(99,179,237,0.1); border-color: rgba(99,179,237,0.35); color: var(--accent-blue); }
1260
+ .role-badge.role-developer { background: rgba(99,179,237,0.08); border-color: rgba(99,179,237,0.25); color: #90cdf4; }
1261
+ .role-badge.role-auditor { background: rgba(154,230,180,0.08); border-color: rgba(154,230,180,0.25); color: #9ae6b4; }
1262
+ .role-badge.role-read-only { background: rgba(183,148,246,0.08); border-color: rgba(183,148,246,0.25); color: #b794f6; }
1263
+ .role-badge.role-developer-role { background: rgba(99,179,237,0.08); border-color: rgba(99,179,237,0.25); color: #90cdf4; }
1264
+ /* Actor detail side panel */
1265
+ .actor-detail-panel {
1266
+ position: fixed; top: 0; right: 0; bottom: 0; width: 360px; max-width: 90vw;
1267
+ background: var(--bg-card); border-left: 1px solid var(--border);
1268
+ transform: translateX(100%); transition: transform 0.2s ease;
1269
+ overflow-y: auto; z-index: 150; padding: 0;
1270
+ }
1271
+ .actor-detail-panel.open { transform: translateX(0); }
1272
+ .adp-header { display: flex; align-items: center; justify-content: space-between; padding: 18px 20px; border-bottom: 1px solid var(--border); }
1273
+ .adp-title { font-size: 1rem; font-weight: 700; }
1274
+ .adp-close { background: none; border: none; color: var(--text-muted); font-size: 1.1rem; cursor: pointer; padding: 4px; line-height: 1; }
1275
+ .adp-close:hover { color: var(--text-primary); }
1276
+ .adp-body { padding: 20px; }
1277
+ .adp-field { margin-bottom: 16px; }
1278
+ .adp-field-lbl { font-size: 0.72rem; color: var(--text-muted); text-transform: uppercase; letter-spacing: 0.06em; margin-bottom: 5px; }
1279
+ .adp-field-val { font-size: 0.9rem; color: var(--text-primary); font-weight: 500; }
1280
+ .adp-field-val code { font-family: 'Courier New', monospace; }
1281
+ .adp-divider { border: none; border-top: 1px solid var(--border); margin: 16px 0; }
1282
+ .adp-chart-wrap { height: 120px; margin-bottom: 4px; }
1283
+ .adp-chart-lbl { font-size: 0.78rem; color: var(--text-muted); margin-bottom: 8px; display: flex; align-items: center; gap: 6px; }
1284
+ .adp-chart-lbl svg { width: 14px; height: 14px; stroke: var(--text-muted); fill: none; stroke-width: 2; stroke-linecap: round; }
1285
+ .adp-jwt-pre {
1286
+ background: var(--bg-body); border: 1px solid var(--border); border-radius: 8px;
1287
+ padding: 12px 14px; font-family: 'Courier New', monospace; font-size: 0.78rem;
1288
+ color: var(--text-secondary); overflow-x: auto; white-space: pre; margin-top: 8px;
1289
+ }
1290
+
1291
+ /* ── Integrations page ── */
1292
+ .integrations-page-header { margin-bottom: 20px; }
1293
+ .integrations-page-header h1 { font-size: 1.4rem; font-weight: 700; margin: 0 0 4px; }
1294
+ .integrations-page-header p { font-size: 0.85rem; color: var(--text-muted); margin: 0; }
1295
+ .integrations-section-title { font-size: 0.95rem; font-weight: 700; margin: 0 0 14px; color: var(--text-primary); }
1296
+ .integrations-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 18px; margin-bottom: 32px; }
1297
+ .integration-card {
1298
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px;
1299
+ }
1300
+ .integration-card-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 16px; }
1301
+ .integration-card-name-row { display: flex; align-items: center; gap: 10px; }
1302
+ .integration-card-name { font-size: 1rem; font-weight: 700; color: var(--text-primary); }
1303
+ .integration-status-badge {
1304
+ padding: 3px 10px; border-radius: 5px; font-size: 0.75rem; font-weight: 700;
1305
+ }
1306
+ .integration-status-badge.healthy { background: rgba(72,187,120,0.15); color: var(--accent-green); border: 1px solid rgba(72,187,120,0.3); }
1307
+ .integration-status-badge.active { background: rgba(72,187,120,0.15); color: var(--accent-green); border: 1px solid rgba(72,187,120,0.3); }
1308
+ .integration-status-badge.degraded { background: rgba(236,201,75,0.15); color: #ecc94b; border: 1px solid rgba(236,201,75,0.3); }
1309
+ .integration-status-badge.inactive { background: rgba(255,255,255,0.06); color: var(--text-muted); border: 1px solid var(--border); }
1310
+ .integration-meta-row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; margin-bottom: 12px; }
1311
+ .integration-meta-lbl { font-size: 0.72rem; color: var(--text-muted); margin-bottom: 3px; }
1312
+ .integration-meta-val { font-size: 0.85rem; color: var(--text-primary); }
1313
+ .integration-meta-val code { font-family: 'Courier New', monospace; font-size: 0.8rem; word-break: break-all; }
1314
+ .integration-flag-row { display: flex; align-items: center; gap: 8px; padding: 7px 12px; border-radius: 6px; font-size: 0.8rem; font-weight: 600; margin-bottom: 10px; }
1315
+ .integration-flag-row.auth-ok { background: rgba(72,187,120,0.1); color: var(--accent-green); border: 1px solid rgba(72,187,120,0.2); }
1316
+ .integration-flag-row.auth-no { background: rgba(255,255,255,0.04); color: var(--text-muted); border: 1px solid var(--border); }
1317
+ .integration-flag-row svg { width: 13px; height: 13px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; }
1318
+ .integration-metrics-row { font-size: 0.82rem; color: var(--text-muted); margin-bottom: 14px; }
1319
+ .integration-metrics-row strong { color: var(--text-primary); }
1320
+ .integration-metrics-row .denied-count { color: var(--accent-red); font-weight: 700; }
1321
+ .btn-view-integration {
1322
+ width: 100%; background: rgba(99,179,237,0.1); border: 1px solid rgba(99,179,237,0.25);
1323
+ color: var(--accent-blue); padding: 9px; border-radius: 8px; font-size: 0.85rem; font-weight: 600; cursor: pointer;
1324
+ }
1325
+ .btn-view-integration:hover { background: rgba(99,179,237,0.18); }
1326
+ .integration-placeholder-card {
1327
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px; padding: 20px;
1328
+ display: flex; align-items: center; justify-content: space-between;
1329
+ }
1330
+ .integration-placeholder-lbl { font-size: 0.9rem; font-weight: 600; color: var(--text-secondary); margin-bottom: 3px; }
1331
+ .integration-placeholder-sub { font-size: 0.78rem; color: var(--text-muted); }
1332
+ .btn-configure-integration {
1333
+ background: var(--bg-body); border: 1px solid var(--border); color: var(--text-secondary);
1334
+ padding: 7px 14px; border-radius: 7px; font-size: 0.8rem; cursor: pointer; white-space: nowrap;
1335
+ }
1336
+ .btn-configure-integration:hover { color: var(--text-primary); }
1337
+
1338
+ /* ── AI Client integration cards ── */
1339
+ .ai-clients-grid {
1340
+ display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; margin-bottom: 32px;
1341
+ }
1342
+ .ai-client-card {
1343
+ background: var(--bg-card); border: 1px solid var(--border); border-radius: 12px;
1344
+ padding: 20px; display: flex; flex-direction: column; gap: 0;
1345
+ }
1346
+ .ai-client-header {
1347
+ display: flex; align-items: flex-start; justify-content: space-between; margin-bottom: 10px;
1348
+ }
1349
+ .ai-client-name-row { display: flex; flex-direction: column; gap: 3px; }
1350
+ .ai-client-icon-name { display: flex; align-items: center; gap: 8px; }
1351
+ .ai-client-icon {
1352
+ width: 28px; height: 28px; border-radius: 6px;
1353
+ display: flex; align-items: center; justify-content: center; flex-shrink: 0;
1354
+ }
1355
+ .ai-client-icon svg { width: 15px; height: 15px; stroke: currentColor; fill: none; stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; }
1356
+ .ai-client-icon.mcp { background: rgba(99,179,237,0.15); color: var(--accent-blue); }
1357
+ .ai-client-icon.openai { background: rgba(72,187,120,0.15); color: var(--accent-green); }
1358
+ .ai-client-icon.plugin { background: rgba(129,230,217,0.15); color: var(--accent-teal); }
1359
+ .ai-client-name { font-size: 0.95rem; font-weight: 700; color: var(--text-primary); }
1360
+ .ai-client-compat { font-size: 0.72rem; color: var(--text-muted); margin-top: 1px; }
1361
+ .ai-client-desc { font-size: 0.8rem; color: var(--text-muted); margin-bottom: 14px; line-height: 1.5; }
1362
+ .ai-client-endpoints { display: flex; flex-direction: column; gap: 6px; margin-bottom: 14px; }
1363
+ .ai-client-endpoint {
1364
+ background: var(--bg-body); border: 1px solid var(--border); border-radius: 7px;
1365
+ padding: 8px 12px; display: flex; align-items: center; gap: 8px;
1366
+ }
1367
+ .ai-endpoint-method {
1368
+ font-size: 0.65rem; font-weight: 700; font-family: monospace; letter-spacing: 0.04em;
1369
+ padding: 2px 6px; border-radius: 3px; flex-shrink: 0;
1370
+ }
1371
+ .ai-endpoint-method.get { background: rgba(72,187,120,0.15); color: var(--accent-green); }
1372
+ .ai-endpoint-method.post { background: rgba(99,179,237,0.15); color: var(--accent-blue); }
1373
+ .ai-endpoint-method.getpost { background: rgba(99,179,237,0.1); color: var(--accent-blue); }
1374
+ .ai-endpoint-path {
1375
+ font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.78rem;
1376
+ color: var(--text-secondary); flex: 1; white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
1377
+ }
1378
+ .ai-client-tools { margin-bottom: 14px; }
1379
+ .ai-client-tools-lbl { font-size: 0.7rem; color: var(--text-muted); font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; }
1380
+ .ai-client-tools-pills { display: flex; flex-wrap: wrap; gap: 5px; }
1381
+ .ai-tool-pill {
1382
+ background: rgba(45,55,72,0.6); border: 1px solid var(--border);
1383
+ border-radius: 4px; padding: 2px 8px;
1384
+ font-family: monospace; font-size: 0.72rem; color: var(--text-muted);
1385
+ }
1386
+ .ai-client-setup {
1387
+ border-top: 1px solid var(--border); padding-top: 12px; margin-top: auto;
1388
+ }
1389
+ .ai-client-setup summary {
1390
+ font-size: 0.78rem; font-weight: 600; color: var(--accent-blue); cursor: pointer;
1391
+ list-style: none; display: flex; align-items: center; gap: 5px;
1392
+ }
1393
+ .ai-client-setup summary::-webkit-details-marker { display: none; }
1394
+ .ai-client-setup summary::before {
1395
+ content: '▶'; font-size: 0.6rem; transition: transform 0.15s;
1396
+ }
1397
+ .ai-client-setup[open] summary::before { transform: rotate(90deg); }
1398
+ .ai-client-snippet {
1399
+ margin-top: 10px; background: var(--bg-body); border: 1px solid var(--border);
1400
+ border-radius: 6px; padding: 10px 12px;
1401
+ font-family: 'SF Mono', 'Fira Code', monospace; font-size: 0.74rem;
1402
+ color: var(--text-secondary); white-space: pre; overflow-x: auto; line-height: 1.55;
1403
+ }
1404
+ .ai-client-snippet-header {
1405
+ display: flex; align-items: center; justify-content: space-between; margin-top: 10px; margin-bottom: 4px;
1406
+ }
1407
+ .ai-client-snippet-lbl { font-size: 0.7rem; color: var(--text-muted); }
1408
+ .btn-copy-snippet {
1409
+ font-size: 0.7rem; padding: 3px 9px; border-radius: 5px; border: 1px solid var(--border);
1410
+ background: var(--bg-card); color: var(--accent-blue); cursor: pointer; transition: background 0.12s;
1411
+ }
1412
+ .btn-copy-snippet:hover { background: rgba(99,179,237,0.12); }
1413
+
1414
+ /* ── Pro / Enterprise upgrade surfaces ── */
1415
+ .pro-badge {
1416
+ display: inline-flex;
1417
+ align-items: center;
1418
+ padding: 2px 7px;
1419
+ border-radius: 4px;
1420
+ font-size: 0.62rem;
1421
+ font-weight: 700;
1422
+ letter-spacing: 0.04em;
1423
+ background: rgba(246,224,94,0.15);
1424
+ color: var(--accent-yellow);
1425
+ border: 1px solid rgba(246,224,94,0.3);
1426
+ text-transform: uppercase;
1427
+ flex-shrink: 0;
1428
+ }
1429
+ .pro-badge.enterprise {
1430
+ background: rgba(183,148,244,0.15);
1431
+ color: var(--accent-purple);
1432
+ border-color: rgba(183,148,244,0.3);
1433
+ }
1434
+ .sidebar-nav .pro-badge { margin-left: auto; }
1435
+ .sidebar.collapsed .sidebar-nav .pro-badge { display: none; }
1436
+
1437
+ .upgrade-banner {
1438
+ display: flex;
1439
+ align-items: center;
1440
+ justify-content: space-between;
1441
+ gap: 16px;
1442
+ padding: 16px 20px;
1443
+ background: linear-gradient(90deg, rgba(99,179,237,0.08), rgba(99,179,237,0.02));
1444
+ border: 1px solid rgba(99,179,237,0.2);
1445
+ border-radius: 10px;
1446
+ margin-bottom: 20px;
1447
+ }
1448
+ .upgrade-banner-text { font-size: 0.88rem; color: var(--text-secondary); }
1449
+ .upgrade-banner-text strong { color: var(--text-primary); font-weight: 600; }
1450
+
1451
+ .btn-contact-sales {
1452
+ display: inline-flex;
1453
+ align-items: center;
1454
+ gap: 6px;
1455
+ padding: 8px 14px;
1456
+ border-radius: 6px;
1457
+ background: var(--accent-blue);
1458
+ color: var(--bg-body);
1459
+ font-size: 0.82rem;
1460
+ font-weight: 600;
1461
+ border: none;
1462
+ cursor: pointer;
1463
+ text-decoration: none;
1464
+ white-space: nowrap;
1465
+ transition: background 0.15s;
1466
+ }
1467
+ .btn-contact-sales:hover { background: var(--accent-blue-bright); }
1468
+
1469
+ .btn-contact-sales-ghost {
1470
+ display: inline-flex;
1471
+ align-items: center;
1472
+ gap: 4px;
1473
+ padding: 6px 10px;
1474
+ border-radius: 6px;
1475
+ background: transparent;
1476
+ color: var(--accent-blue);
1477
+ font-size: 0.8rem;
1478
+ font-weight: 500;
1479
+ border: 1px solid rgba(99,179,237,0.25);
1480
+ cursor: pointer;
1481
+ text-decoration: none;
1482
+ transition: all 0.15s;
1483
+ }
1484
+ .btn-contact-sales-ghost:hover {
1485
+ background: rgba(99,179,237,0.1);
1486
+ border-color: rgba(99,179,237,0.4);
1487
+ }
1488
+
1489
+ .locked-card {
1490
+ position: relative;
1491
+ background: var(--bg-card);
1492
+ border: 1px solid var(--border);
1493
+ border-radius: 10px;
1494
+ padding: 18px;
1495
+ cursor: pointer;
1496
+ transition: border-color 0.15s, transform 0.15s;
1497
+ }
1498
+ .locked-card:hover {
1499
+ border-color: rgba(99,179,237,0.3);
1500
+ transform: translateY(-1px);
1501
+ }
1502
+ .locked-card-header {
1503
+ display: flex;
1504
+ align-items: center;
1505
+ justify-content: space-between;
1506
+ margin-bottom: 10px;
1507
+ }
1508
+ .locked-card-name-row { display: flex; align-items: center; gap: 10px; }
1509
+ .locked-card-icon {
1510
+ width: 28px;
1511
+ height: 28px;
1512
+ display: flex;
1513
+ align-items: center;
1514
+ justify-content: center;
1515
+ color: var(--text-secondary);
1516
+ }
1517
+ .locked-card-icon svg { width: 22px; height: 22px; fill: currentColor; }
1518
+ .locked-card-name { font-size: 0.95rem; font-weight: 600; color: var(--text-primary); }
1519
+ .locked-card-desc { font-size: 0.82rem; color: var(--text-muted); line-height: 1.5; margin-bottom: 14px; }
1520
+ .locked-card-cta {
1521
+ display: inline-flex;
1522
+ align-items: center;
1523
+ gap: 5px;
1524
+ font-size: 0.8rem;
1525
+ font-weight: 500;
1526
+ color: var(--accent-blue);
1527
+ text-decoration: none;
1528
+ }
1529
+
1530
+ .locked-row {
1531
+ display: flex;
1532
+ align-items: center;
1533
+ justify-content: space-between;
1534
+ gap: 16px;
1535
+ padding: 16px 20px;
1536
+ background: var(--bg-card);
1537
+ border: 1px solid var(--border);
1538
+ border-radius: 8px;
1539
+ margin-bottom: 10px;
1540
+ }
1541
+ .locked-row-info { display: flex; flex-direction: column; gap: 4px; min-width: 0; }
1542
+ .locked-row-title {
1543
+ display: flex;
1544
+ align-items: center;
1545
+ gap: 10px;
1546
+ font-size: 0.92rem;
1547
+ font-weight: 600;
1548
+ color: var(--text-primary);
1549
+ }
1550
+ .locked-row-desc { font-size: 0.8rem; color: var(--text-muted); }
1551
+ .lock-icon { width: 13px; height: 13px; stroke: var(--text-muted); fill: none; stroke-width: 2; }
1552
+
1553
+ .retention-pill {
1554
+ display: inline-flex;
1555
+ align-items: center;
1556
+ gap: 6px;
1557
+ padding: 6px 10px;
1558
+ border-radius: 6px;
1559
+ background: rgba(246,224,94,0.08);
1560
+ color: var(--accent-yellow);
1561
+ font-size: 0.75rem;
1562
+ font-weight: 600;
1563
+ border: 1px solid rgba(246,224,94,0.2);
1564
+ cursor: pointer;
1565
+ white-space: nowrap;
1566
+ text-decoration: none;
1567
+ transition: background 0.15s;
1568
+ }
1569
+ .retention-pill:hover { background: rgba(246,224,94,0.15); }
1570
+ .retention-pill .lock-icon { stroke: var(--accent-yellow); }
1571
+
1572
+ .auth-section { margin-bottom: 28px; }
1573
+ .auth-section-title {
1574
+ font-size: 0.95rem;
1575
+ font-weight: 700;
1576
+ color: var(--text-primary);
1577
+ margin: 0 0 12px;
1578
+ }
1579
+
1580
+ /* ── Responsive ── */
1581
+ @media (max-width: 1100px) {
1582
+ .stat-cards { grid-template-columns: repeat(2, 1fr); }
1583
+ .chart-row { grid-template-columns: 1fr; }
1584
+ .lists-row { grid-template-columns: 1fr; }
1585
+ .policies-layout { grid-template-columns: 1fr; }
1586
+ .policies-left { border-right: none; border-bottom: 1px solid var(--border); max-height: 300px; }
1587
+ .search-fields-sidebar { display: none; }
1588
+ .policy-test-grid { grid-template-columns: 1fr; }
1589
+ .integrations-grid { grid-template-columns: 1fr; }
1590
+ .ai-clients-grid { grid-template-columns: 1fr; }
1591
+ }
1592
+
1593
+ @media (max-width: 768px) {
1594
+ .sidebar { display: none; }
1595
+ .main-wrapper { margin-left: 0; }
1596
+ .stat-cards { grid-template-columns: 1fr; }
1597
+ }
1598
+ </style>
1599
+ </head>
1600
+ <body>
1601
+
1602
+ <!-- ── Sidebar ── -->
1603
+ <aside class="sidebar" id="sidebar">
1604
+ <div class="sidebar-brand">
1605
+ <div class="sidebar-brand-icon"></div>
1606
+ <div class="sidebar-brand-text">Oculi<br>Monitor</div>
1607
+ </div>
1608
+ <ul class="sidebar-nav">
1609
+ <li>
1610
+ <a href="/admin/overview" data-page="overview" class="active" data-tooltip="Overview">
1611
+ <svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
1612
+ <span class="nav-label">Overview</span>
1613
+ </a>
1614
+ </li>
1615
+ <li>
1616
+ <a href="/admin/search" data-page="search" data-tooltip="Search">
1617
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1618
+ <span class="nav-label">Search</span>
1619
+ </a>
1620
+ </li>
1621
+ <li>
1622
+ <a href="/admin/policies" data-page="policies" data-tooltip="Policies">
1623
+ <svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
1624
+ <span class="nav-label">Policies</span>
1625
+ </a>
1626
+ </li>
1627
+ <li>
1628
+ <a href="/admin/integrations" data-page="integrations" data-tooltip="Integrations">
1629
+ <svg viewBox="0 0 24 24"><path d="M22 12h-4l-3 9L9 3l-3 9H2"/></svg>
1630
+ <span class="nav-label">Integrations</span>
1631
+ <span class="pro-badge">Pro</span>
1632
+ </a>
1633
+ </li>
1634
+ <li>
1635
+ <a href="/admin/actors" data-page="actors" data-tooltip="Actors & Roles">
1636
+ <svg viewBox="0 0 24 24"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>
1637
+ <span class="nav-label">Actors &amp; Roles</span>
1638
+ </a>
1639
+ </li>
1640
+ </ul>
1641
+ <div class="sidebar-collapse-btn" onclick="toggleSidebar()" id="collapse-btn">
1642
+ <svg class="sidebar-collapse-icon" viewBox="0 0 24 24"><polyline points="15 18 9 12 15 6"/></svg>
1643
+ <span class="nav-label">Collapse</span>
1644
+ </div>
1645
+ </aside>
1646
+
1647
+ <!-- ── Main ── -->
1648
+ <div class="main-wrapper">
1649
+
1650
+ <!-- ── Top bar ── -->
1651
+ <header class="topbar">
1652
+ <div class="topbar-spacer"></div>
1653
+ </header>
1654
+
1655
+ <!-- ── Content ── -->
1656
+ <main class="content">
1657
+
1658
+ <!-- ════════════ Overview Page ════════════ -->
1659
+ <div id="page-overview" class="page active">
1660
+ <h1 class="page-title">Overview Dashboard</h1>
1661
+ <p class="page-subtitle">
1662
+ Real-time security monitoring
1663
+ <button id="refresh-btn" style="margin-left:16px;padding:4px 12px;border-radius:6px;border:1px solid var(--border);background:var(--bg-card);color:var(--text-secondary);font-size:0.8rem;cursor:pointer;">Refresh Now</button>
1664
+ <span id="last-updated" style="margin-left:8px;font-size:0.78rem;color:var(--text-muted);"></span>
1665
+ </p>
1666
+
1667
+ <div class="stat-cards">
1668
+ <div class="stat-card">
1669
+ <div class="stat-card-label">Tool Calls (24h)</div>
1670
+ <div class="stat-card-value" id="stat-total">--</div>
1671
+ </div>
1672
+ <div class="stat-card">
1673
+ <div class="stat-card-label">Denied Calls</div>
1674
+ <div class="stat-card-value" id="stat-denied">--</div>
1675
+ </div>
1676
+ <div class="stat-card">
1677
+ <div class="stat-card-label">Avg Latency</div>
1678
+ <div class="stat-card-value" id="stat-latency">--</div>
1679
+ </div>
1680
+ <div class="stat-card">
1681
+ <div class="stat-card-label">Rate Limit Violations</div>
1682
+ <div class="stat-card-value" id="stat-ratelimit">--</div>
1683
+ </div>
1684
+ </div>
1685
+
1686
+ <div class="chart-row">
1687
+ <div class="card">
1688
+ <div class="card-title">Tool Calls Per Minute</div>
1689
+ <div class="chart-container line-chart">
1690
+ <canvas id="lineChart"></canvas>
1691
+ </div>
1692
+ </div>
1693
+ <div class="card">
1694
+ <div class="card-title">Allowed vs Denied</div>
1695
+ <div class="chart-container pie-chart">
1696
+ <canvas id="pieChart"></canvas>
1697
+ </div>
1698
+ </div>
1699
+ </div>
1700
+
1701
+ <div class="lists-row">
1702
+ <div class="card">
1703
+ <div class="card-title">Top 5 Most Called Tools</div>
1704
+ <div id="top-called"></div>
1705
+ </div>
1706
+ <div class="card">
1707
+ <div class="card-title">Top 5 Denied Tools</div>
1708
+ <div id="top-denied"></div>
1709
+ </div>
1710
+ <div class="card">
1711
+ <div class="card-title">Top Actors by Activity</div>
1712
+ <div id="top-actors"></div>
1713
+ </div>
1714
+ </div>
1715
+ </div>
1716
+
1717
+ <!-- ════════════ Policies Page ════════════ -->
1718
+ <div id="page-policies" class="page">
1719
+ <div class="rules-tab-bar">
1720
+ <button class="rules-tab active" data-tab="visual" onclick="switchPolicyTab('visual')">Visual Rules</button>
1721
+ <button class="rules-tab" data-tab="advanced" onclick="switchPolicyTab('advanced')">Advanced (Rego)</button>
1722
+ </div>
1723
+
1724
+ <!-- Visual Rules View -->
1725
+ <div id="visual-rules-view">
1726
+ <div class="rules-header">
1727
+ <div>
1728
+ <div class="rules-header-title">Security Rules</div>
1729
+ <div class="rules-header-subtitle">Default-allow with deny/warn rules for dangerous patterns</div>
1730
+ </div>
1731
+ <button class="btn-new-policy" onclick="openRuleForm()">+ New Rule</button>
1732
+ </div>
1733
+ <div class="rules-table-wrap">
1734
+ <table class="rules-table" id="rules-table">
1735
+ <thead>
1736
+ <tr>
1737
+ <th>Rule ID</th>
1738
+ <th>Tool</th>
1739
+ <th>Pattern</th>
1740
+ <th>Action</th>
1741
+ <th>Description</th>
1742
+ <th>Enabled</th>
1743
+ <th style="width:80px"></th>
1744
+ </tr>
1745
+ </thead>
1746
+ <tbody id="rules-table-body">
1747
+ </tbody>
1748
+ </table>
1749
+ <div class="rules-empty" id="rules-empty" style="display:none;">No rules yet. Click "+ New Rule" to create one.</div>
1750
+ </div>
1751
+ </div>
1752
+
1753
+ <!-- Advanced Rego View -->
1754
+ <div id="rego-editor-view" style="display:none;">
1755
+ <div class="policies-layout">
1756
+ <!-- Left Panel -->
1757
+ <div class="policies-left">
1758
+ <div class="policies-left-header">
1759
+ <div>
1760
+ <div class="policies-left-title">Policy Editor</div>
1761
+ <div class="policies-left-subtitle">Manage access control policies and test configurations</div>
1762
+ </div>
1763
+ <button class="btn-new-policy" onclick="createNewPolicy()">+ New Policy</button>
1764
+ </div>
1765
+ <div class="policy-cards" id="policy-cards-list">
1766
+ </div>
1767
+ </div>
1768
+
1769
+ <!-- Right Panel -->
1770
+ <div class="policies-right">
1771
+ <div class="policies-right-empty" id="policy-empty-state">
1772
+ Select a policy from the left to view and edit
1773
+ </div>
1774
+ <div id="policy-detail" style="display:none;">
1775
+ <div class="policy-detail-header">
1776
+ <h2 class="policy-detail-name" id="policy-detail-name"></h2>
1777
+ <div class="policy-detail-actions">
1778
+ <button class="btn-policy-action" onclick="editPolicyToggle()" id="btn-edit-policy" title="Edit">
1779
+ <svg viewBox="0 0 24 24"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>
1780
+ Edit
1781
+ </button>
1782
+ <button class="btn-policy-action" onclick="duplicatePolicy()" title="Duplicate">
1783
+ <svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
1784
+ Duplicate
1785
+ </button>
1786
+ <button class="btn-policy-action" onclick="showVersions()" title="Versions">
1787
+ <svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
1788
+ Versions
1789
+ </button>
1790
+ <button class="btn-save-publish" onclick="savePolicy()">Save &amp; Publish</button>
1791
+ </div>
1792
+ </div>
1793
+
1794
+ <div class="policy-editor-area">
1795
+ <textarea class="policy-rego-textarea" id="policy-rego-editor" spellcheck="false" readonly></textarea>
1796
+ </div>
1797
+
1798
+ <div class="policy-test-section">
1799
+ <div class="policy-test-title">Test Policy</div>
1800
+ <div class="policy-test-grid">
1801
+ <div>
1802
+ <div class="policy-test-label">Test Input</div>
1803
+ <textarea class="policy-test-input" id="policy-test-input" spellcheck="false"></textarea>
1804
+ </div>
1805
+ <div>
1806
+ <div class="policy-test-label">Decision Output</div>
1807
+ <div class="policy-test-output" id="policy-test-output">Run test to see result</div>
1808
+ </div>
1809
+ </div>
1810
+ <div class="policy-test-buttons">
1811
+ <button class="btn-run-test" onclick="runPolicyTest()">
1812
+ <svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"><polygon points="5 3 19 12 5 21 5 3"/></svg>
1813
+ Run Test
1814
+ </button>
1815
+ <button class="btn-load-sample" onclick="loadSampleInput()">Load Sample</button>
1816
+ </div>
1817
+ </div>
1818
+ </div>
1819
+ </div>
1820
+ </div>
1821
+ </div>
1822
+ </div>
1823
+
1824
+ <!-- ── Search Page ── -->
1825
+ <div id="page-search" class="page">
1826
+
1827
+ <!-- KQL bar -->
1828
+ <div class="kql-topbar">
1829
+ <div class="kql-bar">
1830
+ <input type="text" class="kql-input" id="kql-input"
1831
+ placeholder='actor="alice" | stats count by tool'
1832
+ onkeydown="if(event.key==='Enter') runKQLSearch()">
1833
+ <button class="btn-kql-search" onclick="runKQLSearch()">
1834
+ <svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
1835
+ Search
1836
+ </button>
1837
+ </div>
1838
+ <div class="time-picker-wrap" id="time-picker-wrap">
1839
+ <button class="btn-time-picker" id="btn-time-picker" onclick="toggleTimePicker()">
1840
+ <svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;"><rect x="3" y="4" width="18" height="18" rx="2"/><line x1="16" y1="2" x2="16" y2="6"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="3" y1="10" x2="21" y2="10"/></svg>
1841
+ <span id="time-range-label">Last 24 hours</span>
1842
+ <svg viewBox="0 0 24 24" style="width:10px;height:10px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="6 9 12 15 18 9"/></svg>
1843
+ </button>
1844
+ <div class="time-picker-dropdown" id="time-picker-dropdown">
1845
+ <div class="tp-section-lbl">Quick Select</div>
1846
+ <div class="tp-presets" id="tp-presets"></div>
1847
+ <hr class="tp-divider">
1848
+ <div class="tp-section-lbl">Custom Range</div>
1849
+ <div class="tp-custom-grid">
1850
+ <div>
1851
+ <div class="tp-field-lbl">From</div>
1852
+ <input type="datetime-local" id="tp-from" class="tp-datetime-input" step="60">
1853
+ </div>
1854
+ <div>
1855
+ <div class="tp-field-lbl">To</div>
1856
+ <input type="datetime-local" id="tp-to" class="tp-datetime-input" step="60">
1857
+ </div>
1858
+ </div>
1859
+ <button class="btn-tp-apply" onclick="applyCustomTimeRange()">Apply Custom Range</button>
1860
+ </div>
1861
+ </div>
1862
+ <button class="btn-export-kql" onclick="exportSearchResults()">
1863
+ <svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
1864
+ Export
1865
+ </button>
1866
+ <a class="retention-pill" href="#" onclick="event.preventDefault();openSales('retention');" title="Free tier retains 30 days of audit logs. Upgrade for longer retention.">
1867
+ <svg class="lock-icon" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
1868
+ Retention: 30 days
1869
+ </a>
1870
+ </div>
1871
+
1872
+ <!-- Filter chips row -->
1873
+ <div class="kql-filter-row">
1874
+ <div class="filter-panel-wrap" id="filter-panel-wrap">
1875
+ <button class="btn-add-filter" id="btn-add-filter" onclick="toggleFilterPanel()">+ Add Filter</button>
1876
+ <div class="filter-panel-dropdown" id="filter-panel-dropdown">
1877
+ <div class="fp-section-lbl">Decision</div>
1878
+ <div class="fp-btn-row">
1879
+ <button class="btn-fp-toggle" id="fp-dec-allow" onclick="toggleFilterDecision('allow')">Allow</button>
1880
+ <button class="btn-fp-toggle" id="fp-dec-deny" onclick="toggleFilterDecision('deny')">Deny</button>
1881
+ </div>
1882
+ <div class="fp-spacer">
1883
+ <div class="fp-section-lbl">Actor</div>
1884
+ <input type="text" id="fp-actor" class="fp-text-input" placeholder="e.g., agent-prod-01"
1885
+ onkeydown="if(event.key==='Enter')addFilterFromPanel('actor','fp-actor')">
1886
+ <div class="fp-hint">Press Enter to add</div>
1887
+ </div>
1888
+ <div class="fp-spacer">
1889
+ <div class="fp-section-lbl">Tool</div>
1890
+ <input type="text" id="fp-tool" class="fp-text-input" placeholder="e.g., github:list_repos"
1891
+ onkeydown="if(event.key==='Enter')addFilterFromPanel('tool','fp-tool')">
1892
+ <div class="fp-hint">Press Enter to add</div>
1893
+ </div>
1894
+ <div class="fp-spacer">
1895
+ <div class="fp-section-lbl">Upstream</div>
1896
+ <input type="text" id="fp-upstream" class="fp-text-input" placeholder="e.g., github-api"
1897
+ onkeydown="if(event.key==='Enter')addFilterFromPanel('upstream','fp-upstream')">
1898
+ <div class="fp-hint">Press Enter to add</div>
1899
+ </div>
1900
+ </div>
1901
+ </div>
1902
+ <div id="filter-chips"></div>
1903
+ <span class="kql-result-meta" id="kql-result-meta">— events | Last 24h</span>
1904
+ </div>
1905
+
1906
+ <!-- Fields sidebar + main results -->
1907
+ <div class="search-layout">
1908
+ <div class="search-fields-sidebar" id="search-fields-sidebar"></div>
1909
+
1910
+ <div class="search-main">
1911
+ <!-- Tabs + timeline -->
1912
+ <div class="search-tabs-card">
1913
+ <div class="search-tabs">
1914
+ <button class="search-tab active" id="stab-events" onclick="setSearchTab('events')">Events (<span id="stab-events-count">—</span>)</button>
1915
+ <button class="search-tab" id="stab-aggregation" onclick="setSearchTab('aggregation')" style="display:none;">Aggregation (<span id="stab-aggregation-count">—</span>)</button>
1916
+ </div>
1917
+
1918
+ <!-- Events tab -->
1919
+ <div class="search-tab-panel active" id="stab-panel-events">
1920
+ <div class="search-timeline-wrap">
1921
+ <div class="search-timeline-hdr">
1922
+ <span class="search-timeline-lbl">Format Timeline</span>
1923
+ <span class="search-timeline-lbl" id="tl-density">1 event per column</span>
1924
+ </div>
1925
+ <div class="search-timeline-bar" id="search-timeline"></div>
1926
+ </div>
1927
+ </div>
1928
+ <!-- Aggregation tab -->
1929
+ <div class="search-tab-panel" id="stab-panel-aggregation">
1930
+ <div style="padding:24px 20px;" id="aggregation-content"><span style="color:var(--text-muted);font-size:0.85rem;">Run a <code>| stats count by</code> query to see aggregations.</span></div>
1931
+ </div>
1932
+ </div>
1933
+
1934
+ <!-- Results table -->
1935
+ <div class="search-results-wrap">
1936
+ <table class="search-log-table">
1937
+ <thead>
1938
+ <tr>
1939
+ <th style="width:150px;">Time</th>
1940
+ <th>Event</th>
1941
+ </tr>
1942
+ </thead>
1943
+ <tbody id="search-tbody">
1944
+ <tr><td colspan="2"><div class="slt-row"><span class="search-empty" style="padding:32px 0;">Loading…</span></div></td></tr>
1945
+ </tbody>
1946
+ </table>
1947
+ </div>
1948
+
1949
+ <div class="search-pagination" id="search-pagination"></div>
1950
+ </div><!-- /.search-main -->
1951
+ </div><!-- /.search-layout -->
1952
+ </div>
1953
+
1954
+ <!-- ════════════ Actors & Roles Page ════════════ -->
1955
+ <!-- ════════════ Integrations Page (Pro sales surface) ════════════ -->
1956
+ <div id="page-integrations" class="page">
1957
+ <div class="integrations-page-header">
1958
+ <h1>Integrations</h1>
1959
+ <p>Send policy events and alerts to your existing tooling.</p>
1960
+ </div>
1961
+ <div class="upgrade-banner">
1962
+ <div class="upgrade-banner-text">
1963
+ <strong>Integrations are a Pro feature.</strong> Wire allow/deny events into Slack, PagerDuty, SIEM, and your ticketing systems.
1964
+ </div>
1965
+ <a class="btn-contact-sales" href="#" onclick="event.preventDefault();openSales('integrations');">
1966
+ Contact sales
1967
+ <svg viewBox="0 0 24 24" style="width:13px;height:13px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="9 18 15 12 9 6"/></svg>
1968
+ </a>
1969
+ </div>
1970
+ <div class="integrations-grid" id="locked-integrations-grid">
1971
+ <div class="locked-card" onclick="openSales('integrations')">
1972
+ <div class="locked-card-header">
1973
+ <div class="locked-card-name-row">
1974
+ <div class="locked-card-icon"><svg viewBox="0 0 24 24"><path d="M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zm1.271 0a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zm0 1.271a2.527 2.527 0 0 1 2.521 2.521 2.527 2.527 0 0 1-2.521 2.521H2.522A2.527 2.527 0 0 1 0 8.834a2.527 2.527 0 0 1 2.522-2.521h6.312zm10.122 2.521a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zm-1.268 0a2.527 2.527 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zm-2.523 10.122a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zm0-1.268a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z"/></svg></div>
1975
+ <span class="locked-card-name">Slack</span>
1976
+ </div>
1977
+ <span class="pro-badge">Pro</span>
1978
+ </div>
1979
+ <div class="locked-card-desc">Post denied calls and policy violations to a Slack channel in real time.</div>
1980
+ <span class="locked-card-cta">Contact sales <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="9 18 15 12 9 6"/></svg></span>
1981
+ </div>
1982
+ <div class="locked-card" onclick="openSales('integrations')">
1983
+ <div class="locked-card-header">
1984
+ <div class="locked-card-name-row">
1985
+ <div class="locked-card-icon"><svg viewBox="0 0 24 24"><path d="M22.563 12.418c0 .284-.232.515-.516.515H11.564a.516.516 0 0 1 0-1.031h10.483c.284 0 .516.232.516.516zm-.516-4.987H11.564a.516.516 0 0 0 0 1.031h10.483a.516.516 0 0 0 0-1.031zm0 9.974H11.564a.516.516 0 0 0 0 1.031h10.483a.516.516 0 0 0 0-1.031zM3.45 7.431h6.062a.516.516 0 0 0 0-1.031H3.45a.516.516 0 0 0 0 1.031zm0 5.502h6.062a.516.516 0 0 0 0-1.031H3.45a.516.516 0 0 0 0 1.031zm0 5.503h6.062a.516.516 0 0 0 0-1.031H3.45a.516.516 0 0 0 0 1.031z"/></svg></div>
1986
+ <span class="locked-card-name">PagerDuty</span>
1987
+ </div>
1988
+ <span class="pro-badge">Pro</span>
1989
+ </div>
1990
+ <div class="locked-card-desc">Page on-call when critical policies trigger or anomalous tool usage is detected.</div>
1991
+ <span class="locked-card-cta">Contact sales <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="9 18 15 12 9 6"/></svg></span>
1992
+ </div>
1993
+ <div class="locked-card" onclick="openSales('integrations')">
1994
+ <div class="locked-card-header">
1995
+ <div class="locked-card-name-row">
1996
+ <div class="locked-card-icon"><svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M8 14l4-8 4 8" fill="none" stroke="currentColor" stroke-width="2"/></svg></div>
1997
+ <span class="locked-card-name">Datadog</span>
1998
+ </div>
1999
+ <span class="pro-badge">Pro</span>
2000
+ </div>
2001
+ <div class="locked-card-desc">Forward audit logs and metrics into your existing Datadog observability pipeline.</div>
2002
+ <span class="locked-card-cta">Contact sales <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="9 18 15 12 9 6"/></svg></span>
2003
+ </div>
2004
+ <div class="locked-card" onclick="openSales('integrations')">
2005
+ <div class="locked-card-header">
2006
+ <div class="locked-card-name-row">
2007
+ <div class="locked-card-icon"><svg viewBox="0 0 24 24"><path d="M11.571 11.513H0a5.218 5.218 0 0 0 5.232 5.215h2.13v2.057A5.215 5.215 0 0 0 12.575 24V12.518a1.005 1.005 0 0 0-1.005-1.005zm5.723-5.756H5.736a5.215 5.215 0 0 0 5.215 5.214h2.129v2.058a5.218 5.218 0 0 0 5.215 5.214V6.762a1.005 1.005 0 0 0-1.001-1.005zM23.013 0H11.479a5.215 5.215 0 0 0 5.215 5.214h2.129v2.057A5.215 5.215 0 0 0 24 12.483V1.005A1.005 1.005 0 0 0 23.013 0z"/></svg></div>
2008
+ <span class="locked-card-name">Jira</span>
2009
+ </div>
2010
+ <span class="pro-badge">Pro</span>
2011
+ </div>
2012
+ <div class="locked-card-desc">Open a Jira ticket when a high-severity policy violation is recorded.</div>
2013
+ <span class="locked-card-cta">Contact sales <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="9 18 15 12 9 6"/></svg></span>
2014
+ </div>
2015
+ <div class="locked-card" onclick="openSales('integrations')">
2016
+ <div class="locked-card-header">
2017
+ <div class="locked-card-name-row">
2018
+ <div class="locked-card-icon"><svg viewBox="0 0 24 24"><path d="M12 .297c-6.63 0-12 5.373-12 12 0 5.303 3.438 9.8 8.205 11.385.6.113.82-.258.82-.577 0-.285-.01-1.04-.015-2.04-3.338.724-4.042-1.61-4.042-1.61C4.422 18.07 3.633 17.7 3.633 17.7c-1.087-.744.084-.729.084-.729 1.205.084 1.838 1.236 1.838 1.236 1.07 1.835 2.809 1.305 3.495.998.108-.776.417-1.305.76-1.605-2.665-.3-5.466-1.332-5.466-5.93 0-1.31.465-2.38 1.235-3.22-.135-.303-.54-1.523.105-3.176 0 0 1.005-.322 3.3 1.23.96-.267 1.98-.399 3-.405 1.02.006 2.04.138 3 .405 2.28-1.552 3.285-1.23 3.285-1.23.645 1.653.24 2.873.12 3.176.765.84 1.23 1.91 1.23 3.22 0 4.61-2.805 5.625-5.475 5.92.42.36.81 1.096.81 2.22 0 1.606-.015 2.896-.015 3.286 0 .315.21.69.825.57C20.565 22.092 24 17.592 24 12.297c0-6.627-5.373-12-12-12"/></svg></div>
2019
+ <span class="locked-card-name">GitHub</span>
2020
+ </div>
2021
+ <span class="pro-badge">Pro</span>
2022
+ </div>
2023
+ <div class="locked-card-desc">Push policy violations as comments on the PR that triggered them, with full audit context.</div>
2024
+ <span class="locked-card-cta">Contact sales <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="9 18 15 12 9 6"/></svg></span>
2025
+ </div>
2026
+ <div class="locked-card" onclick="openSales('integrations')">
2027
+ <div class="locked-card-header">
2028
+ <div class="locked-card-name-row">
2029
+ <div class="locked-card-icon"><svg viewBox="0 0 24 24"><path d="M3.51 9.225h6.84c.451 0 .818.366.818.818v6.84a.818.818 0 0 1-.818.818H3.51a.818.818 0 0 1-.818-.818v-6.84c0-.452.367-.818.818-.818zm10.14 0h6.84c.451 0 .818.366.818.818v6.84a.818.818 0 0 1-.818.818h-6.84a.818.818 0 0 1-.818-.818v-6.84c0-.452.367-.818.818-.818z"/></svg></div>
2030
+ <span class="locked-card-name">Linear</span>
2031
+ </div>
2032
+ <span class="pro-badge">Pro</span>
2033
+ </div>
2034
+ <div class="locked-card-desc">Create Linear issues for unresolved policy violations and track remediation in your workflow.</div>
2035
+ <span class="locked-card-cta">Contact sales <svg viewBox="0 0 24 24" style="width:12px;height:12px;stroke:currentColor;fill:none;stroke-width:2.5;stroke-linecap:round;"><polyline points="9 18 15 12 9 6"/></svg></span>
2036
+ </div>
2037
+ </div>
2038
+ </div>
2039
+
2040
+ <!-- ════════════ Actors & Roles Page ════════════ -->
2041
+ <div id="page-actors" class="page">
2042
+ <div class="actors-page-header">
2043
+ <h1>Actors &amp; Roles</h1>
2044
+ <p>Manage actors, roles, and access permissions</p>
2045
+ </div>
2046
+
2047
+ <div class="auth-section">
2048
+ <h2 class="auth-section-title">Authentication</h2>
2049
+ <div class="locked-row" onclick="openSales('sso')">
2050
+ <div class="locked-row-info">
2051
+ <div class="locked-row-title">
2052
+ Single Sign-On (SAML / OIDC)
2053
+ <span class="pro-badge enterprise">Enterprise</span>
2054
+ <svg class="lock-icon" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
2055
+ </div>
2056
+ <div class="locked-row-desc">Let your team sign in with Okta, Google Workspace, Microsoft Entra, or any SAML/OIDC provider.</div>
2057
+ </div>
2058
+ <a class="btn-contact-sales-ghost" href="#" onclick="event.preventDefault();event.stopPropagation();openSales('sso');">Contact sales →</a>
2059
+ </div>
2060
+ <div class="locked-row" onclick="openSales('sso')">
2061
+ <div class="locked-row-info">
2062
+ <div class="locked-row-title">
2063
+ SCIM user provisioning
2064
+ <span class="pro-badge enterprise">Enterprise</span>
2065
+ <svg class="lock-icon" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>
2066
+ </div>
2067
+ <div class="locked-row-desc">Auto-provision and deprovision actors from your identity provider as employees join or leave.</div>
2068
+ </div>
2069
+ <a class="btn-contact-sales-ghost" href="#" onclick="event.preventDefault();event.stopPropagation();openSales('sso');">Contact sales →</a>
2070
+ </div>
2071
+ </div>
2072
+
2073
+ <div class="actors-table-card">
2074
+ <table class="actors-table">
2075
+ <thead>
2076
+ <tr>
2077
+ <th>Actor ID</th>
2078
+ <th>Type</th>
2079
+ <th>Roles</th>
2080
+ <th>Organization</th>
2081
+ <th>Tool Access</th>
2082
+ </tr>
2083
+ </thead>
2084
+ <tbody id="actors-tbody">
2085
+ <tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-muted);">Loading…</td></tr>
2086
+ </tbody>
2087
+ </table>
2088
+ </div>
2089
+ </div>
2090
+
2091
+ </main>
2092
+ </div>
2093
+
2094
+ <!-- ── Actor Detail Panel ── -->
2095
+ <div class="actor-detail-panel" id="actor-detail-panel">
2096
+ <div class="adp-header">
2097
+ <span class="adp-title">Actor Details</span>
2098
+ <button class="adp-close" onclick="closeActorPanel()" title="Close">&#10005;</button>
2099
+ </div>
2100
+ <div class="adp-body">
2101
+ <div class="adp-field">
2102
+ <div class="adp-field-lbl">Actor ID</div>
2103
+ <div class="adp-field-val"><code id="adp-id">—</code></div>
2104
+ </div>
2105
+ <div class="adp-field">
2106
+ <div class="adp-field-lbl">Type</div>
2107
+ <div class="adp-field-val" id="adp-type">—</div>
2108
+ </div>
2109
+ <div class="adp-field">
2110
+ <div class="adp-field-lbl">Roles</div>
2111
+ <div class="adp-field-val" id="adp-roles">—</div>
2112
+ </div>
2113
+ <div class="adp-field">
2114
+ <div class="adp-field-lbl">Organization</div>
2115
+ <div class="adp-field-val" id="adp-org">—</div>
2116
+ </div>
2117
+ <div class="adp-field">
2118
+ <div class="adp-field-lbl">Tool Access Scope</div>
2119
+ <div class="adp-field-val" id="adp-access">—</div>
2120
+ </div>
2121
+ <hr class="adp-divider">
2122
+ <div class="adp-chart-lbl">
2123
+ <svg viewBox="0 0 24 24"><polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/></svg>
2124
+ Tool Call History (24h)
2125
+ </div>
2126
+ <div class="adp-chart-wrap">
2127
+ <canvas id="actor-mini-chart"></canvas>
2128
+ </div>
2129
+ <hr class="adp-divider">
2130
+ <div class="adp-field-lbl" style="margin-bottom:6px;">JWT Claims Preview</div>
2131
+ <pre class="adp-jwt-pre" id="adp-jwt">—</pre>
2132
+ </div>
2133
+ </div>
2134
+
2135
+ <!-- ── Call Details Panel ── -->
2136
+ <div class="call-details-overlay" id="call-details-overlay" onclick="closeCallDetails()"></div>
2137
+ <div class="call-details-panel" id="call-details-panel">
2138
+ <div class="cdp-header">
2139
+ <span class="cdp-title">Call Details</span>
2140
+ <button class="cdp-close" onclick="closeCallDetails()" title="Close">&#10005;</button>
2141
+ </div>
2142
+ <div class="cdp-body" id="cdp-body"></div>
2143
+ </div>
2144
+
2145
+ <!-- ── Rule Form Panel ── -->
2146
+ <div class="call-details-overlay" id="rule-form-overlay" onclick="closeRuleForm()"></div>
2147
+ <div class="call-details-panel" id="rule-form-panel">
2148
+ <div class="cdp-header">
2149
+ <span class="cdp-title" id="rule-form-title">New Rule</span>
2150
+ <button class="cdp-close" onclick="closeRuleForm()" title="Close">&#10005;</button>
2151
+ </div>
2152
+ <div class="cdp-body">
2153
+ <div class="rule-form-group">
2154
+ <label class="rule-form-label">Rule ID</label>
2155
+ <input class="rule-form-input mono" id="rule-form-id" placeholder="e.g. no-rm-rf" />
2156
+ </div>
2157
+ <div class="rule-form-group">
2158
+ <label class="rule-form-label">Tool (exact match)</label>
2159
+ <select class="rule-form-select" id="rule-form-tool">
2160
+ <option value="">Any tool</option>
2161
+ <option value="shell">shell</option>
2162
+ <option value="file_read">file_read</option>
2163
+ <option value="file_edit">file_edit</option>
2164
+ <option value="mcp_call">mcp_call</option>
2165
+ </select>
2166
+ </div>
2167
+ <div class="rule-form-group">
2168
+ <label class="rule-form-label">Command Pattern (regex)</label>
2169
+ <input class="rule-form-input mono" id="rule-form-command" placeholder='e.g. rm\s+-rf' />
2170
+ </div>
2171
+ <div class="rule-form-group">
2172
+ <label class="rule-form-label">File Pattern (regex)</label>
2173
+ <input class="rule-form-input mono" id="rule-form-file" placeholder='e.g. \.env' />
2174
+ </div>
2175
+ <div class="rule-form-group">
2176
+ <label class="rule-form-label">MCP Server (exact match)</label>
2177
+ <input class="rule-form-input" id="rule-form-mcp" placeholder="e.g. filesystem-server" />
2178
+ </div>
2179
+ <div class="rule-form-group">
2180
+ <label class="rule-form-label">Action</label>
2181
+ <select class="rule-form-select" id="rule-form-action">
2182
+ <option value="deny">Deny</option>
2183
+ <option value="warn">Warn</option>
2184
+ <option value="allow">Allow</option>
2185
+ </select>
2186
+ </div>
2187
+ <div class="rule-form-group">
2188
+ <label class="rule-form-label">Description</label>
2189
+ <input class="rule-form-input" id="rule-form-desc" placeholder="What does this rule do?" />
2190
+ </div>
2191
+ <div class="rule-form-group">
2192
+ <label class="rule-form-label">Priority</label>
2193
+ <input class="rule-form-input" id="rule-form-priority" type="number" placeholder="0" value="0" />
2194
+ </div>
2195
+ <div style="display:flex;gap:10px;margin-top:8px;">
2196
+ <button class="btn-new-policy" onclick="saveRule()" id="rule-form-save">Save Rule</button>
2197
+ <button class="btn-policy-action" onclick="closeRuleForm()">Cancel</button>
2198
+ </div>
2199
+ </div>
2200
+ </div>
2201
+
2202
+ <script>
2203
+ // ── API ──
2204
+ const API_BASE = window.location.origin;
2205
+
2206
+ // ── Marketing / sales surface ──
2207
+ const MARKETING_URL = 'https://oculisecurity.com';
2208
+ function openSales(_feature) {
2209
+ window.open(MARKETING_URL + '/pricing', '_blank', 'noopener');
2210
+ }
2211
+
2212
+ async function fetchJSON(path) {
2213
+ const res = await fetch(API_BASE + path);
2214
+ if (!res.ok) throw new Error(path + ': ' + res.status);
2215
+ return res.json();
2216
+ }
2217
+
2218
+ // ── Render lists ──
2219
+ function renderList(containerId, items, colorClass) {
2220
+ const container = document.getElementById(containerId);
2221
+ if (!items.length) {
2222
+ container.innerHTML = '<div class="list-item"><div class="list-item-left"><span class="list-item-name" style="color:var(--text-muted)">No data yet</span></div></div>';
2223
+ return;
2224
+ }
2225
+ container.innerHTML = items.map((item, i) =>
2226
+ '<div class="list-item">' +
2227
+ '<div class="list-item-left">' +
2228
+ '<span class="list-item-rank">#' + (i + 1) + '</span>' +
2229
+ '<span class="list-item-name">' + item.name + '</span>' +
2230
+ '</div>' +
2231
+ '<span class="list-item-value ' + colorClass + '">' + item.count.toLocaleString() + '</span>' +
2232
+ '</div>'
2233
+ ).join('');
2234
+ }
2235
+
2236
+ // ── Charts ──
2237
+ let lineChartInstance = null;
2238
+ let pieChartInstance = null;
2239
+
2240
+ function updateCharts(timeseries, decisions) {
2241
+ const labels = timeseries.map(function(p) {
2242
+ const d = new Date(p.bucket);
2243
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
2244
+ });
2245
+ const data = timeseries.map(function(p) { return p.count; });
2246
+
2247
+ if (lineChartInstance) lineChartInstance.destroy();
2248
+ lineChartInstance = new Chart(document.getElementById('lineChart'), {
2249
+ type: 'line',
2250
+ data: {
2251
+ labels: labels,
2252
+ datasets: [{
2253
+ data: data,
2254
+ borderColor: '#63b3ed',
2255
+ backgroundColor: 'rgba(99, 179, 237, 0.08)',
2256
+ fill: true,
2257
+ tension: 0.3,
2258
+ pointBackgroundColor: '#63b3ed',
2259
+ pointBorderColor: '#63b3ed',
2260
+ pointRadius: 4,
2261
+ pointHoverRadius: 6,
2262
+ borderWidth: 2,
2263
+ }],
2264
+ },
2265
+ options: {
2266
+ responsive: true,
2267
+ maintainAspectRatio: false,
2268
+ plugins: { legend: { display: false } },
2269
+ scales: {
2270
+ x: {
2271
+ grid: { color: 'rgba(45,55,72,0.4)' },
2272
+ ticks: { color: '#718096', font: { size: 11 } },
2273
+ },
2274
+ y: {
2275
+ beginAtZero: true,
2276
+ grid: { color: 'rgba(45,55,72,0.4)' },
2277
+ ticks: { color: '#718096', font: { size: 11 } },
2278
+ },
2279
+ },
2280
+ },
2281
+ });
2282
+
2283
+ if (pieChartInstance) pieChartInstance.destroy();
2284
+ pieChartInstance = new Chart(document.getElementById('pieChart'), {
2285
+ type: 'doughnut',
2286
+ data: {
2287
+ labels: ['Allowed', 'Denied'],
2288
+ datasets: [{
2289
+ data: [decisions.allowed, decisions.denied],
2290
+ backgroundColor: ['#48bb78', '#fc8181'],
2291
+ borderColor: ['#48bb78', '#fc8181'],
2292
+ borderWidth: 1,
2293
+ hoverOffset: 6,
2294
+ }],
2295
+ },
2296
+ options: {
2297
+ responsive: true,
2298
+ maintainAspectRatio: false,
2299
+ cutout: '55%',
2300
+ plugins: {
2301
+ legend: {
2302
+ position: 'right',
2303
+ labels: {
2304
+ color: '#a0aec0',
2305
+ font: { size: 13 },
2306
+ padding: 20,
2307
+ usePointStyle: true,
2308
+ pointStyleWidth: 12,
2309
+ },
2310
+ },
2311
+ },
2312
+ },
2313
+ });
2314
+ }
2315
+
2316
+ // ── Dashboard loader ──
2317
+ async function loadDashboard() {
2318
+ try {
2319
+ const [summary, topTools, topDenied, topActors, decisions, timeseries] =
2320
+ await Promise.all([
2321
+ fetchJSON('/api/admin/stats/summary?hours=24'),
2322
+ fetchJSON('/api/admin/stats/top-tools?limit=5'),
2323
+ fetchJSON('/api/admin/stats/top-denied?limit=5'),
2324
+ fetchJSON('/api/admin/stats/top-actors?limit=5'),
2325
+ fetchJSON('/api/admin/stats/decisions'),
2326
+ fetchJSON('/api/admin/stats/timeseries?hours=1&bucket=5'),
2327
+ ]);
2328
+
2329
+ // Stat cards
2330
+ document.getElementById('stat-total').textContent = summary.totalCalls.toLocaleString();
2331
+ document.getElementById('stat-denied').textContent = summary.deniedCalls.toLocaleString();
2332
+ document.getElementById('stat-latency').innerHTML =
2333
+ summary.avgLatencyMs + '<span style="font-size:1rem;font-weight:400;">ms</span>';
2334
+ document.getElementById('stat-ratelimit').textContent =
2335
+ summary.rateLimitViolations.toLocaleString();
2336
+
2337
+ // Lists
2338
+ renderList('top-called', topTools.map(function(t) { return { name: t.tool, count: t.count }; }), 'blue');
2339
+ renderList('top-denied', topDenied.map(function(t) { return { name: t.tool, count: t.count }; }), 'red');
2340
+ renderList('top-actors', topActors.map(function(a) { return { name: a.actor, count: a.count }; }), 'green');
2341
+
2342
+ // Charts
2343
+ updateCharts(timeseries, decisions);
2344
+
2345
+ // Timestamp
2346
+ var ts = document.getElementById('last-updated');
2347
+ if (ts) ts.textContent = 'Updated ' + new Date().toLocaleTimeString();
2348
+ } catch (err) {
2349
+ console.error('Dashboard load failed:', err);
2350
+ }
2351
+ }
2352
+
2353
+ // ── Settings loader ──
2354
+ function escapeHtml(str) {
2355
+ var div = document.createElement('div');
2356
+ div.textContent = str;
2357
+ return div.innerHTML;
2358
+ }
2359
+
2360
+ // ── Policies page ──
2361
+ var policiesLoaded = false;
2362
+ var policiesList = [];
2363
+ var selectedPolicyId = null;
2364
+ var policyEditMode = false;
2365
+
2366
+ async function loadPolicies() {
2367
+ try {
2368
+ var data = await fetchJSON('/api/admin/policies');
2369
+ policiesList = data.policies;
2370
+ renderPolicyCards();
2371
+ if (policiesList.length > 0 && !selectedPolicyId) {
2372
+ selectPolicy(policiesList[0].id);
2373
+ } else if (selectedPolicyId) {
2374
+ var current = policiesList.find(function(p) { return p.id === selectedPolicyId; });
2375
+ if (current) renderPolicyDetail(current);
2376
+ else { selectedPolicyId = null; renderPolicyCards(); }
2377
+ }
2378
+ policiesLoaded = true;
2379
+ } catch (err) {
2380
+ console.error('Policies load failed:', err);
2381
+ }
2382
+ }
2383
+
2384
+ function renderPolicyCards() {
2385
+ var container = document.getElementById('policy-cards-list');
2386
+ if (!policiesList.length) {
2387
+ container.innerHTML = '<div style="color:var(--text-muted);font-size:0.85rem;padding:20px 0;">No policies yet. Click "+ New Policy" to create one.</div>';
2388
+ return;
2389
+ }
2390
+ container.innerHTML = policiesList.map(function(p) {
2391
+ var isSelected = p.id === selectedPolicyId;
2392
+ var updatedDate = p.updatedAt ? p.updatedAt.replace('T', ' ').replace(/\.\d+Z$/, '').replace('Z', '') : '';
2393
+ return '<div class="policy-card' + (isSelected ? ' selected' : '') + '" onclick="selectPolicy(' + p.id + ')">' +
2394
+ '<div class="policy-card-name">' + escapeHtml(p.name) + '</div>' +
2395
+ '<div class="policy-card-meta">' +
2396
+ '<span class="policy-version-badge">v' + escapeHtml(p.version) + '</span>' +
2397
+ '<span class="policy-card-updated">Updated ' + escapeHtml(updatedDate) + '<br>by ' + escapeHtml(p.updatedBy) + '</span>' +
2398
+ '</div>' +
2399
+ '</div>';
2400
+ }).join('');
2401
+ }
2402
+
2403
+ function selectPolicy(id) {
2404
+ selectedPolicyId = id;
2405
+ policyEditMode = false;
2406
+ renderPolicyCards();
2407
+ var policy = policiesList.find(function(p) { return p.id === id; });
2408
+ if (policy) renderPolicyDetail(policy);
2409
+ }
2410
+
2411
+ function renderPolicyDetail(policy) {
2412
+ document.getElementById('policy-empty-state').style.display = 'none';
2413
+ document.getElementById('policy-detail').style.display = 'block';
2414
+ document.getElementById('policy-detail-name').textContent = policy.name;
2415
+
2416
+ var editor = document.getElementById('policy-rego-editor');
2417
+ editor.value = policy.rego;
2418
+ editor.readOnly = true;
2419
+ policyEditMode = false;
2420
+ updateEditButton();
2421
+
2422
+ // Reset test output
2423
+ var output = document.getElementById('policy-test-output');
2424
+ output.textContent = 'Run test to see result';
2425
+ output.style.color = 'var(--text-muted)';
2426
+ }
2427
+
2428
+ function updateEditButton() {
2429
+ var btn = document.getElementById('btn-edit-policy');
2430
+ if (policyEditMode) {
2431
+ btn.style.background = 'rgba(99, 179, 237, 0.15)';
2432
+ btn.style.color = 'var(--accent-blue)';
2433
+ btn.style.borderColor = 'var(--accent-blue)';
2434
+ } else {
2435
+ btn.style.background = '';
2436
+ btn.style.color = '';
2437
+ btn.style.borderColor = '';
2438
+ }
2439
+ }
2440
+
2441
+ function editPolicyToggle() {
2442
+ policyEditMode = !policyEditMode;
2443
+ var editor = document.getElementById('policy-rego-editor');
2444
+ editor.readOnly = !policyEditMode;
2445
+ editor.style.opacity = policyEditMode ? '1' : '0.85';
2446
+ updateEditButton();
2447
+ if (policyEditMode) editor.focus();
2448
+ }
2449
+
2450
+ async function savePolicy() {
2451
+ if (!selectedPolicyId) return;
2452
+ var policy = policiesList.find(function(p) { return p.id === selectedPolicyId; });
2453
+ if (!policy) return;
2454
+ var rego = document.getElementById('policy-rego-editor').value;
2455
+ try {
2456
+ var res = await fetch(API_BASE + '/api/admin/policies/' + selectedPolicyId, {
2457
+ method: 'PUT',
2458
+ headers: { 'Content-Type': 'application/json' },
2459
+ body: JSON.stringify({ rego: rego, updatedBy: policy.updatedBy }),
2460
+ });
2461
+ if (!res.ok) throw new Error('Save failed: ' + res.status);
2462
+ policyEditMode = false;
2463
+ policiesLoaded = false;
2464
+ await loadPolicies();
2465
+ } catch (err) {
2466
+ console.error('Save policy failed:', err);
2467
+ alert('Failed to save policy. Check console for details.');
2468
+ }
2469
+ }
2470
+
2471
+ async function createNewPolicy() {
2472
+ var name = prompt('Policy name:');
2473
+ if (!name) return;
2474
+ try {
2475
+ var res = await fetch(API_BASE + '/api/admin/policies', {
2476
+ method: 'POST',
2477
+ headers: { 'Content-Type': 'application/json' },
2478
+ body: JSON.stringify({
2479
+ name: name,
2480
+ rego: 'package gateway.authz\n\ndefault allow := false\n\n# Add your policy rules here\n',
2481
+ updatedBy: 'admin@oculi.security',
2482
+ }),
2483
+ });
2484
+ if (!res.ok) throw new Error('Create failed: ' + res.status);
2485
+ var created = await res.json();
2486
+ selectedPolicyId = created.id;
2487
+ policiesLoaded = false;
2488
+ await loadPolicies();
2489
+ } catch (err) {
2490
+ console.error('Create policy failed:', err);
2491
+ alert('Failed to create policy. Check console for details.');
2492
+ }
2493
+ }
2494
+
2495
+ async function duplicatePolicy() {
2496
+ if (!selectedPolicyId) return;
2497
+ try {
2498
+ var res = await fetch(API_BASE + '/api/admin/policies/' + selectedPolicyId + '/duplicate', {
2499
+ method: 'POST',
2500
+ });
2501
+ if (!res.ok) throw new Error('Duplicate failed: ' + res.status);
2502
+ var duplicated = await res.json();
2503
+ selectedPolicyId = duplicated.id;
2504
+ policiesLoaded = false;
2505
+ await loadPolicies();
2506
+ } catch (err) {
2507
+ console.error('Duplicate policy failed:', err);
2508
+ alert('Failed to duplicate policy. Check console for details.');
2509
+ }
2510
+ }
2511
+
2512
+ function showVersions() {
2513
+ alert('Version history coming soon. Current version is shown on the policy card.');
2514
+ }
2515
+
2516
+ async function runPolicyTest() {
2517
+ var inputEl = document.getElementById('policy-test-input');
2518
+ var outputEl = document.getElementById('policy-test-output');
2519
+ try {
2520
+ var inputJson = JSON.parse(inputEl.value);
2521
+ outputEl.textContent = 'Evaluating...';
2522
+ outputEl.style.color = 'var(--text-muted)';
2523
+ var res = await fetch(API_BASE + '/api/admin/policies/test', {
2524
+ method: 'POST',
2525
+ headers: { 'Content-Type': 'application/json' },
2526
+ body: JSON.stringify({ input: inputJson }),
2527
+ });
2528
+ var result = await res.json();
2529
+ if (!res.ok) {
2530
+ outputEl.textContent = 'Error: ' + (result.error || res.status);
2531
+ outputEl.style.color = 'var(--accent-red)';
2532
+ return;
2533
+ }
2534
+ outputEl.textContent = JSON.stringify(result.decision, null, 2);
2535
+ outputEl.style.color = result.decision.allow ? 'var(--accent-green)' : 'var(--accent-red)';
2536
+ } catch (err) {
2537
+ outputEl.textContent = 'Invalid JSON input: ' + err.message;
2538
+ outputEl.style.color = 'var(--accent-red)';
2539
+ }
2540
+ }
2541
+
2542
+ function loadSampleInput() {
2543
+ var sample = {
2544
+ actor: 'alice',
2545
+ orgId: 'org-1',
2546
+ roles: ['editor'],
2547
+ upstreamId: 'fs-server',
2548
+ tool: 'readFile',
2549
+ args: { path: 'hello.txt' },
2550
+ time: new Date().toISOString(),
2551
+ ip: '127.0.0.1',
2552
+ sessionId: 'test-session-1'
2553
+ };
2554
+ document.getElementById('policy-test-input').value = JSON.stringify(sample, null, 2);
2555
+ }
2556
+
2557
+ // ── Visual Rules ──
2558
+ var visualRulesList = [];
2559
+ var editingRuleDbId = null; // null = creating new, number = editing existing
2560
+ var activePolicyTab = 'visual';
2561
+
2562
+ function switchPolicyTab(tab) {
2563
+ activePolicyTab = tab;
2564
+ document.querySelectorAll('.rules-tab').forEach(function(btn) {
2565
+ btn.classList.toggle('active', btn.getAttribute('data-tab') === tab);
2566
+ });
2567
+ document.getElementById('visual-rules-view').style.display = tab === 'visual' ? '' : 'none';
2568
+ document.getElementById('rego-editor-view').style.display = tab === 'advanced' ? '' : 'none';
2569
+ if (tab === 'visual') loadVisualRules();
2570
+ if (tab === 'advanced') loadPolicies();
2571
+ }
2572
+
2573
+ async function loadVisualRules() {
2574
+ try {
2575
+ var data = await fetchJSON('/api/admin/rules');
2576
+ visualRulesList = data.rules;
2577
+ renderRulesTable();
2578
+ } catch (err) {
2579
+ console.error('Visual rules load failed:', err);
2580
+ }
2581
+ }
2582
+
2583
+ function renderRulesTable() {
2584
+ var tbody = document.getElementById('rules-table-body');
2585
+ var empty = document.getElementById('rules-empty');
2586
+ var table = document.getElementById('rules-table');
2587
+ if (!visualRulesList.length) {
2588
+ tbody.innerHTML = '';
2589
+ table.style.display = 'none';
2590
+ empty.style.display = '';
2591
+ return;
2592
+ }
2593
+ table.style.display = '';
2594
+ empty.style.display = 'none';
2595
+ tbody.innerHTML = visualRulesList.map(function(r) {
2596
+ var pattern = r.command_pattern || r.file_pattern || r.mcp_server || '-';
2597
+ var toolLabel = r.tool || 'Any';
2598
+ var checked = r.enabled ? 'checked' : '';
2599
+ return '<tr>' +
2600
+ '<td class="rule-id-cell">' + escapeHtml(r.rule_id) + '</td>' +
2601
+ '<td>' + escapeHtml(toolLabel) + '</td>' +
2602
+ '<td class="rule-pattern-cell">' + escapeHtml(pattern) + '</td>' +
2603
+ '<td><span class="rule-action-badge ' + r.action + '">' + r.action.toUpperCase() + '</span></td>' +
2604
+ '<td class="rule-desc-cell" title="' + escapeHtml(r.description || '') + '">' + escapeHtml(r.description || '-') + '</td>' +
2605
+ '<td><label class="st-switch"><input type="checkbox" ' + checked + ' onchange="toggleRuleEnabled(' + r.id + ', this.checked)"><span class="st-slider"></span></label></td>' +
2606
+ '<td><div class="rule-actions-cell">' +
2607
+ '<button class="rule-action-btn" onclick="openRuleForm(' + r.id + ')" title="Edit">' +
2608
+ '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"><path d="M11 4H4a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7"/><path d="M18.5 2.5a2.121 2.121 0 0 1 3 3L12 15l-4 1 1-4 9.5-9.5z"/></svg>' +
2609
+ '</button>' +
2610
+ '<button class="rule-action-btn delete" onclick="deleteRule(' + r.id + ')" title="Delete">' +
2611
+ '<svg viewBox="0 0 24 24" style="width:14px;height:14px;stroke:currentColor;fill:none;stroke-width:2;stroke-linecap:round;stroke-linejoin:round;"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/></svg>' +
2612
+ '</button>' +
2613
+ '</div></td>' +
2614
+ '</tr>';
2615
+ }).join('');
2616
+ }
2617
+
2618
+ function openRuleForm(ruleDbId) {
2619
+ editingRuleDbId = ruleDbId || null;
2620
+ var title = document.getElementById('rule-form-title');
2621
+ if (editingRuleDbId) {
2622
+ title.textContent = 'Edit Rule';
2623
+ var rule = visualRulesList.find(function(r) { return r.id === editingRuleDbId; });
2624
+ if (rule) {
2625
+ document.getElementById('rule-form-id').value = rule.rule_id;
2626
+ document.getElementById('rule-form-tool').value = rule.tool || '';
2627
+ document.getElementById('rule-form-command').value = rule.command_pattern || '';
2628
+ document.getElementById('rule-form-file').value = rule.file_pattern || '';
2629
+ document.getElementById('rule-form-mcp').value = rule.mcp_server || '';
2630
+ document.getElementById('rule-form-action').value = rule.action;
2631
+ document.getElementById('rule-form-desc').value = rule.description || '';
2632
+ document.getElementById('rule-form-priority').value = rule.priority || 0;
2633
+ }
2634
+ } else {
2635
+ title.textContent = 'New Rule';
2636
+ document.getElementById('rule-form-id').value = '';
2637
+ document.getElementById('rule-form-tool').value = '';
2638
+ document.getElementById('rule-form-command').value = '';
2639
+ document.getElementById('rule-form-file').value = '';
2640
+ document.getElementById('rule-form-mcp').value = '';
2641
+ document.getElementById('rule-form-action').value = 'deny';
2642
+ document.getElementById('rule-form-desc').value = '';
2643
+ document.getElementById('rule-form-priority').value = '0';
2644
+ }
2645
+ document.getElementById('rule-form-overlay').classList.add('open');
2646
+ document.getElementById('rule-form-panel').classList.add('open');
2647
+ document.getElementById('rule-form-id').focus();
2648
+ }
2649
+
2650
+ function closeRuleForm() {
2651
+ editingRuleDbId = null;
2652
+ document.getElementById('rule-form-overlay').classList.remove('open');
2653
+ document.getElementById('rule-form-panel').classList.remove('open');
2654
+ }
2655
+
2656
+ async function saveRule() {
2657
+ var ruleId = document.getElementById('rule-form-id').value.trim();
2658
+ var tool = document.getElementById('rule-form-tool').value || null;
2659
+ var commandPattern = document.getElementById('rule-form-command').value.trim() || null;
2660
+ var filePattern = document.getElementById('rule-form-file').value.trim() || null;
2661
+ var mcpServer = document.getElementById('rule-form-mcp').value.trim() || null;
2662
+ var action = document.getElementById('rule-form-action').value;
2663
+ var description = document.getElementById('rule-form-desc').value.trim();
2664
+ var priority = parseInt(document.getElementById('rule-form-priority').value, 10) || 0;
2665
+
2666
+ if (!ruleId) { alert('Rule ID is required'); return; }
2667
+ if (!tool && !commandPattern && !filePattern && !mcpServer) {
2668
+ alert('At least one match field (tool, command pattern, file pattern, or MCP server) is required');
2669
+ return;
2670
+ }
2671
+
2672
+ var body = {
2673
+ rule_id: ruleId,
2674
+ tool: tool,
2675
+ command_pattern: commandPattern,
2676
+ file_pattern: filePattern,
2677
+ mcp_server: mcpServer,
2678
+ action: action,
2679
+ description: description,
2680
+ priority: priority
2681
+ };
2682
+
2683
+ try {
2684
+ var url, method;
2685
+ if (editingRuleDbId) {
2686
+ url = API_BASE + '/api/admin/rules/' + editingRuleDbId;
2687
+ method = 'PUT';
2688
+ } else {
2689
+ url = API_BASE + '/api/admin/rules';
2690
+ method = 'POST';
2691
+ }
2692
+ var res = await fetch(url, {
2693
+ method: method,
2694
+ headers: { 'Content-Type': 'application/json' },
2695
+ body: JSON.stringify(body),
2696
+ });
2697
+ if (!res.ok) {
2698
+ var errData = await res.json().catch(function() { return {}; });
2699
+ alert(errData.error || 'Save failed: ' + res.status);
2700
+ return;
2701
+ }
2702
+ closeRuleForm();
2703
+ await loadVisualRules();
2704
+ } catch (err) {
2705
+ console.error('Save rule failed:', err);
2706
+ alert('Failed to save rule. Check console for details.');
2707
+ }
2708
+ }
2709
+
2710
+ async function deleteRule(id) {
2711
+ if (!confirm('Delete this rule?')) return;
2712
+ try {
2713
+ var res = await fetch(API_BASE + '/api/admin/rules/' + id, { method: 'DELETE' });
2714
+ if (!res.ok) throw new Error('Delete failed: ' + res.status);
2715
+ await loadVisualRules();
2716
+ } catch (err) {
2717
+ console.error('Delete rule failed:', err);
2718
+ alert('Failed to delete rule.');
2719
+ }
2720
+ }
2721
+
2722
+ async function toggleRuleEnabled(id, enabled) {
2723
+ try {
2724
+ var res = await fetch(API_BASE + '/api/admin/rules/' + id, {
2725
+ method: 'PUT',
2726
+ headers: { 'Content-Type': 'application/json' },
2727
+ body: JSON.stringify({ enabled: enabled ? 1 : 0 }),
2728
+ });
2729
+ if (!res.ok) throw new Error('Toggle failed: ' + res.status);
2730
+ await loadVisualRules();
2731
+ } catch (err) {
2732
+ console.error('Toggle rule failed:', err);
2733
+ }
2734
+ }
2735
+
2736
+ // ── Search page (KQL) ──
2737
+ var searchPage = 0;
2738
+ var searchPageSize = 50;
2739
+ var currentSearchResults = [];
2740
+ var currentSearchTotal = 0;
2741
+ var expandedRowIdx = null;
2742
+ var activeSearchFilters = []; // [{key, value}]
2743
+ var currentSearchTab = 'events';
2744
+ var sfsExpandedFields = {}; // track which sidebar fields are expanded
2745
+
2746
+ // Time picker state
2747
+ var timePickerOpen = false;
2748
+ var timeRangePresets = [
2749
+ { label: 'Last 15 min', hours: 0.25 },
2750
+ { label: 'Last 1 hour', hours: 1 },
2751
+ { label: 'Last 4 hours', hours: 4 },
2752
+ { label: 'Last 24 hours', hours: 24 },
2753
+ { label: 'Last 7 days', hours: 168 },
2754
+ { label: 'Last 30 days', hours: 720 },
2755
+ { label: 'Last 90 days', hours: 2160 },
2756
+ { label: 'All time', hours: 0 },
2757
+ ];
2758
+ var timeRangeActiveIdx = 3; // default: Last 24 hours
2759
+ var timeRangeCustomFrom = null;
2760
+ var timeRangeCustomTo = null;
2761
+
2762
+ function toggleTimePicker() {
2763
+ timePickerOpen = !timePickerOpen;
2764
+ var dd = document.getElementById('time-picker-dropdown');
2765
+ var btn = document.getElementById('btn-time-picker');
2766
+ dd.classList.toggle('open', timePickerOpen);
2767
+ btn.classList.toggle('active', timePickerOpen);
2768
+ if (timePickerOpen) renderTimePickerPresets();
2769
+ }
2770
+
2771
+ function renderTimePickerPresets() {
2772
+ var el = document.getElementById('tp-presets');
2773
+ el.innerHTML = timeRangePresets.map(function(p, i) {
2774
+ var cls = 'btn-tp-preset' + (timeRangeCustomFrom === null && timeRangeCustomTo === null && i === timeRangeActiveIdx ? ' active' : '');
2775
+ return '<button class="' + cls + '" onclick="selectTimePreset(' + i + ')">' + p.label + '</button>';
2776
+ }).join('');
2777
+ }
2778
+
2779
+ function selectTimePreset(idx) {
2780
+ timeRangeActiveIdx = idx;
2781
+ timeRangeCustomFrom = null;
2782
+ timeRangeCustomTo = null;
2783
+ document.getElementById('tp-from').value = '';
2784
+ document.getElementById('tp-to').value = '';
2785
+ document.getElementById('time-range-label').textContent = timeRangePresets[idx].label;
2786
+ closeTimePicker();
2787
+ runSearch(true);
2788
+ }
2789
+
2790
+ function applyCustomTimeRange() {
2791
+ var fromVal = document.getElementById('tp-from').value;
2792
+ var toVal = document.getElementById('tp-to').value;
2793
+ if (!fromVal && !toVal) { return; }
2794
+ timeRangeCustomFrom = fromVal ? new Date(fromVal).toISOString() : null;
2795
+ timeRangeCustomTo = toVal ? new Date(toVal).toISOString() : null;
2796
+ timeRangeActiveIdx = -1;
2797
+ // Compact label: "MM/DD HH:MM → MM/DD HH:MM" — drop the year to save space
2798
+ function fmtDtl(v) {
2799
+ // datetime-local value is "YYYY-MM-DDTHH:MM", keep "MM/DD HH:MM"
2800
+ var parts = v.slice(0, 16).split('T');
2801
+ var dateParts = parts[0].split('-');
2802
+ return dateParts[1] + '/' + dateParts[2] + ' ' + (parts[1] || '');
2803
+ }
2804
+ var lbl = (fromVal ? fmtDtl(fromVal) : 'start') + ' → ' + (toVal ? fmtDtl(toVal) : 'now');
2805
+ document.getElementById('time-range-label').textContent = lbl;
2806
+ closeTimePicker();
2807
+ runSearch(true);
2808
+ }
2809
+
2810
+ function closeTimePicker() {
2811
+ timePickerOpen = false;
2812
+ document.getElementById('time-picker-dropdown').classList.remove('open');
2813
+ document.getElementById('btn-time-picker').classList.remove('active');
2814
+ }
2815
+
2816
+ // Close dropdowns on outside click
2817
+ document.addEventListener('click', function(e) {
2818
+ if (timePickerOpen) {
2819
+ var wrap = document.getElementById('time-picker-wrap');
2820
+ if (wrap && !wrap.contains(e.target)) closeTimePicker();
2821
+ }
2822
+ if (filterPanelOpen) {
2823
+ var fpWrap = document.getElementById('filter-panel-wrap');
2824
+ if (fpWrap && !fpWrap.contains(e.target)) closeFilterPanel();
2825
+ }
2826
+ });
2827
+
2828
+ // ── KQL parser ──
2829
+ // Returns { filters: { q, decision }, aggregation: null | { groupBy: string[] } }
2830
+ function parseKQL(raw) {
2831
+ var filters = {};
2832
+ var aggregation = null;
2833
+ if (!raw) return { filters: filters, aggregation: aggregation };
2834
+
2835
+ // Split on first pipe
2836
+ var pipeIdx = raw.indexOf('|');
2837
+ var filterPart = pipeIdx >= 0 ? raw.slice(0, pipeIdx) : raw;
2838
+ var pipelinePart = pipeIdx >= 0 ? raw.slice(pipeIdx + 1).trim() : '';
2839
+
2840
+ // Parse filter part
2841
+ var dm = filterPart.match(/decision\s*==?\s*"?(allow|deny)"?/i);
2842
+ if (dm) filters.decision = dm[1].toLowerCase();
2843
+ // actor / tool / upstream = or == "*text*" or "text"
2844
+ var sm = filterPart.match(/(?:search\s+)?(?:actor|tool|upstream)\s*==?\s*"?\*?([^"*\s|]+)\*?"?/i);
2845
+ if (sm) filters.q = sm[1];
2846
+ // bare: search "term" without a field name
2847
+ if (!filters.q) {
2848
+ var bsm = filterPart.match(/\bsearch\s+"([^"]+)"/i);
2849
+ if (bsm) filters.q = bsm[1];
2850
+ }
2851
+ // plain text fallback
2852
+ if (!filters.q && !dm) {
2853
+ var plain = filterPart
2854
+ .replace(/^\s*(?:where|search)\s+/i, '')
2855
+ .replace(/^\*$/, '')
2856
+ .trim();
2857
+ if (plain) filters.q = plain;
2858
+ }
2859
+
2860
+ // Parse pipeline part: stats count by field1, field2, ...
2861
+ if (pipelinePart) {
2862
+ var agg = pipelinePart.match(/^\s*stats\s+count\s+by\s+(.+)$/i);
2863
+ if (agg) {
2864
+ var groupBy = agg[1].split(',').map(function(s) { return s.trim(); }).filter(Boolean);
2865
+ if (groupBy.length) aggregation = { groupBy: groupBy };
2866
+ }
2867
+ }
2868
+
2869
+ return { filters: filters, aggregation: aggregation };
2870
+ }
2871
+
2872
+ // ── Filter panel state ──
2873
+ var filterPanelOpen = false;
2874
+ var filterPanelDecision = null; // 'allow' | 'deny' | null
2875
+
2876
+ function toggleFilterPanel() {
2877
+ filterPanelOpen = !filterPanelOpen;
2878
+ document.getElementById('filter-panel-dropdown').classList.toggle('open', filterPanelOpen);
2879
+ if (filterPanelOpen) syncFilterPanelToggles();
2880
+ }
2881
+
2882
+ function closeFilterPanel() {
2883
+ filterPanelOpen = false;
2884
+ document.getElementById('filter-panel-dropdown').classList.remove('open');
2885
+ }
2886
+
2887
+ function syncFilterPanelToggles() {
2888
+ ['allow', 'deny'].forEach(function(v) {
2889
+ var el = document.getElementById('fp-dec-' + v);
2890
+ if (el) el.classList.toggle('active', filterPanelDecision === v);
2891
+ });
2892
+ }
2893
+
2894
+ function toggleFilterDecision(val) {
2895
+ filterPanelDecision = filterPanelDecision === val ? null : val;
2896
+ syncFilterPanelToggles();
2897
+ activeSearchFilters = activeSearchFilters.filter(function(f) { return f.key !== 'decision'; });
2898
+ if (filterPanelDecision) activeSearchFilters.push({ key: 'decision', value: filterPanelDecision });
2899
+ renderFilterChips();
2900
+ runSearch(true);
2901
+ }
2902
+
2903
+ function addFilterFromPanel(key, inputId) {
2904
+ var val = document.getElementById(inputId).value.trim();
2905
+ if (!val) return;
2906
+ activeSearchFilters = activeSearchFilters.filter(function(f) { return f.key !== key; });
2907
+ activeSearchFilters.push({ key: key, value: val });
2908
+ document.getElementById(inputId).value = '';
2909
+ renderFilterChips();
2910
+ runSearch(true);
2911
+ }
2912
+
2913
+ // ── Filter chips ──
2914
+ function renderFilterChips() {
2915
+ var el = document.getElementById('filter-chips');
2916
+ el.innerHTML = activeSearchFilters.map(function(f, i) {
2917
+ return '<span class="filter-chip">' +
2918
+ '<span class="kv-key">' + escapeHtml(f.key) + '</span>' +
2919
+ '<span class="kv-eq">=</span>' +
2920
+ '<span style="color:var(--text-primary)">' + escapeHtml(f.value) + '</span>' +
2921
+ ' <em class="filter-chip-x" onclick="removeFilter(' + i + ')">&#10005;</em>' +
2922
+ '</span>';
2923
+ }).join('');
2924
+ }
2925
+
2926
+ function removeFilter(idx) {
2927
+ var removed = activeSearchFilters.splice(idx, 1)[0];
2928
+ if (removed) {
2929
+ if (removed.key === 'decision') filterPanelDecision = null;
2930
+ }
2931
+ syncFilterPanelToggles();
2932
+ renderFilterChips();
2933
+ runSearch(true);
2934
+ }
2935
+
2936
+
2937
+ // ── Tabs ──
2938
+ function setSearchTab(tab) {
2939
+ currentSearchTab = tab;
2940
+ document.querySelectorAll('.search-tab').forEach(function(b) { b.classList.remove('active'); });
2941
+ document.getElementById('stab-' + tab).classList.add('active');
2942
+ document.querySelectorAll('.search-tab-panel').forEach(function(p) { p.classList.remove('active'); });
2943
+ document.getElementById('stab-panel-' + tab).classList.add('active');
2944
+ }
2945
+
2946
+ // ── Timeline ──
2947
+ function renderTimeline(results) {
2948
+ var bar = document.getElementById('search-timeline');
2949
+ if (!results.length) { bar.innerHTML = ''; return; }
2950
+ var hours = timeRangeActiveIdx >= 0 ? (timeRangePresets[timeRangeActiveIdx].hours || 24) : 24;
2951
+ var bucketMin = hours <= 1 ? 5 : hours <= 24 ? 60 : hours <= 168 ? 360 : 1440;
2952
+ fetch(API_BASE + '/api/admin/stats/timeseries?hours=' + hours + '&bucket=' + bucketMin)
2953
+ .then(function(r) { return r.json(); })
2954
+ .then(function(buckets) {
2955
+ if (!buckets.length) { bar.innerHTML = ''; return; }
2956
+ var max = Math.max.apply(null, buckets.map(function(b) { return b.count; })) || 1;
2957
+ var total = buckets.reduce(function(s, b) { return s + b.count; }, 0);
2958
+ document.getElementById('tl-density').textContent =
2959
+ total + ' event' + (total !== 1 ? 's' : '') + ' · ' + buckets.length + ' buckets';
2960
+ bar.innerHTML = buckets.map(function(b) {
2961
+ var pct = Math.max(8, Math.round((b.count / max) * 100));
2962
+ var alpha = 0.15 + (b.count / max) * 0.65;
2963
+ return '<div class="tl-bucket" title="' + b.bucket + ': ' + b.count + ' events" ' +
2964
+ 'style="height:' + pct + '%;background:rgba(72,187,120,' + alpha.toFixed(2) + ');"></div>';
2965
+ }).join('');
2966
+ }).catch(function() { bar.innerHTML = ''; });
2967
+ }
2968
+
2969
+ // ── Main search ──
2970
+ // kqlParams holds filter params parsed from the KQL input
2971
+ var kqlParams = {};
2972
+ var kqlAggregation = null;
2973
+ var currentSearchIsAggregation = false;
2974
+
2975
+ function loadSearch() { runSearch(true); }
2976
+
2977
+ function runKQLSearch() {
2978
+ var raw = document.getElementById('kql-input').value.trim();
2979
+ var parsed = parseKQL(raw);
2980
+ kqlParams = parsed.filters;
2981
+ kqlAggregation = parsed.aggregation;
2982
+ // Do NOT touch activeSearchFilters — chips are independent of KQL search
2983
+ runSearch(true);
2984
+ }
2985
+
2986
+ function buildTimeParams(params) {
2987
+ if (timeRangeCustomFrom) {
2988
+ params.set('from', timeRangeCustomFrom);
2989
+ } else if (timeRangeActiveIdx >= 0 && timeRangePresets[timeRangeActiveIdx].hours > 0) {
2990
+ var fromTs = new Date(Date.now() - timeRangePresets[timeRangeActiveIdx].hours * 3600000).toISOString();
2991
+ params.set('from', fromTs);
2992
+ }
2993
+ if (timeRangeCustomTo) params.set('to', timeRangeCustomTo);
2994
+ }
2995
+
2996
+ function runSearch(resetPage) {
2997
+ if (resetPage) { searchPage = 0; expandedRowIdx = null; }
2998
+
2999
+ if (kqlAggregation) {
3000
+ runAggregationSearch();
3001
+ } else {
3002
+ runEventsSearch();
3003
+ }
3004
+ }
3005
+
3006
+ function runEventsSearch() {
3007
+ // Hide aggregation tab, ensure events tab is active
3008
+ var aggTab = document.getElementById('stab-aggregation');
3009
+ if (aggTab) aggTab.style.display = 'none';
3010
+ setSearchTab('events');
3011
+
3012
+ var params = new URLSearchParams();
3013
+ var q = kqlParams.q || null;
3014
+ var decision = kqlParams.decision || null;
3015
+ activeSearchFilters.forEach(function(f) {
3016
+ if (f.key === 'decision') decision = f.value;
3017
+ else q = f.value;
3018
+ });
3019
+ if (q) params.set('q', q);
3020
+ if (decision) params.set('decision', decision);
3021
+ params.set('limit', String(searchPageSize));
3022
+ params.set('offset', String(searchPage * searchPageSize));
3023
+ buildTimeParams(params);
3024
+
3025
+ document.getElementById('search-tbody').innerHTML =
3026
+ '<tr><td colspan="2"><div class="search-no-results" style="padding:40px 0;"><div class="search-no-results-icon" style="font-size:1.4rem;">&#9203;</div><div class="search-no-results-title" style="font-size:0.85rem;">Searching…</div></div></td></tr>';
3027
+
3028
+ fetch(API_BASE + '/api/admin/search?' + params.toString())
3029
+ .then(function(r) { return r.json(); })
3030
+ .then(function(data) { renderSearchResults(data); })
3031
+ .catch(function(err) {
3032
+ document.getElementById('search-tbody').innerHTML =
3033
+ '<tr><td colspan="2"><div class="slt-row"><span style="color:var(--accent-red);font-size:0.82rem;">Error: ' + escapeHtml(String(err.message)) + '</span></div></td></tr>';
3034
+ });
3035
+ }
3036
+
3037
+ function runAggregationSearch() {
3038
+ // Show and activate aggregation tab
3039
+ var aggTab = document.getElementById('stab-aggregation');
3040
+ if (aggTab) aggTab.style.display = '';
3041
+ setSearchTab('aggregation');
3042
+
3043
+ var params = new URLSearchParams();
3044
+ params.set('groupBy', kqlAggregation.groupBy.join(','));
3045
+ var q = kqlParams.q || null;
3046
+ var decision = kqlParams.decision || null;
3047
+ activeSearchFilters.forEach(function(f) {
3048
+ if (f.key === 'decision') decision = f.value;
3049
+ else q = f.value;
3050
+ });
3051
+ if (q) params.set('q', q);
3052
+ if (decision) params.set('decision', decision);
3053
+ buildTimeParams(params);
3054
+
3055
+ document.getElementById('aggregation-content').innerHTML =
3056
+ '<span style="color:var(--text-muted);font-size:0.85rem;">Aggregating…</span>';
3057
+
3058
+ fetch(API_BASE + '/api/admin/aggregate?' + params.toString())
3059
+ .then(function(r) { return r.json(); })
3060
+ .then(function(data) { renderAggregationResults(data, kqlAggregation.groupBy); })
3061
+ .catch(function(err) {
3062
+ document.getElementById('aggregation-content').innerHTML =
3063
+ '<span style="color:var(--accent-red);font-size:0.82rem;">Error: ' + escapeHtml(String(err.message)) + '</span>';
3064
+ });
3065
+ }
3066
+
3067
+ function renderAggregationResults(data, groupBy) {
3068
+ var results = data.results || [];
3069
+ var rangeLabel = timeRangeActiveIdx >= 0
3070
+ ? timeRangePresets[timeRangeActiveIdx].label
3071
+ : (timeRangeCustomFrom || timeRangeCustomTo ? 'Custom range' : 'All time');
3072
+
3073
+ document.getElementById('kql-result-meta').textContent =
3074
+ results.length.toLocaleString() + ' group' + (results.length !== 1 ? 's' : '') +
3075
+ ' | Group by: ' + groupBy.join(', ') + ' | ' + rangeLabel;
3076
+ document.getElementById('stab-aggregation-count').textContent = results.length.toLocaleString();
3077
+
3078
+ if (!results.length) {
3079
+ document.getElementById('aggregation-content').innerHTML =
3080
+ '<div style="padding:24px 20px;"><span style="color:var(--text-muted);font-size:0.85rem;">No results found.</span></div>';
3081
+ return;
3082
+ }
3083
+
3084
+ // Build table columns: groupBy fields + Count
3085
+ var cols = groupBy.concat(['count']);
3086
+ var thHtml = cols.map(function(c) {
3087
+ return '<th style="text-align:' + (c === 'count' ? 'right' : 'left') + ';padding:8px 12px;color:var(--text-muted);font-size:0.72rem;text-transform:uppercase;border-bottom:1px solid var(--border);">' + escapeHtml(c) + '</th>';
3088
+ }).join('');
3089
+
3090
+ var trHtml = results.map(function(row) {
3091
+ return '<tr>' + cols.map(function(c) {
3092
+ var val = row[c] != null ? String(row[c]) : '—';
3093
+ return '<td style="padding:7px 12px;border-bottom:1px solid rgba(45,55,72,0.4);' +
3094
+ (c === 'count' ? 'text-align:right;font-weight:600;' : 'font-family:monospace;') + '">' +
3095
+ escapeHtml(val) + '</td>';
3096
+ }).join('') + '</tr>';
3097
+ }).join('');
3098
+
3099
+ document.getElementById('aggregation-content').innerHTML =
3100
+ '<table style="width:100%;border-collapse:collapse;font-size:0.83rem;">' +
3101
+ '<thead><tr>' + thHtml + '</tr></thead>' +
3102
+ '<tbody>' + trHtml + '</tbody>' +
3103
+ '</table>';
3104
+ }
3105
+
3106
+ function renderSearchResults(data) {
3107
+ var results = data.results || [];
3108
+ var total = data.total || 0;
3109
+ currentSearchResults = results;
3110
+ currentSearchTotal = total;
3111
+
3112
+ var rangeLabel = timeRangeActiveIdx >= 0
3113
+ ? timeRangePresets[timeRangeActiveIdx].label
3114
+ : (timeRangeCustomFrom || timeRangeCustomTo ? 'Custom range' : 'All time');
3115
+ document.getElementById('kql-result-meta').textContent =
3116
+ total.toLocaleString() + ' event' + (total !== 1 ? 's' : '') + ' | ' + rangeLabel;
3117
+ document.getElementById('stab-events-count').textContent = total.toLocaleString();
3118
+
3119
+ renderTimeline(results);
3120
+ renderFieldsSidebar(results);
3121
+
3122
+ var tbody = document.getElementById('search-tbody');
3123
+ if (!results.length) {
3124
+ var hasFilters = activeSearchFilters.length > 0;
3125
+ var emptyHtml =
3126
+ '<div class="search-no-results">' +
3127
+ '<div class="search-no-results-icon">&#128269;</div>' +
3128
+ '<div class="search-no-results-title">No events found</div>' +
3129
+ '<div class="search-no-results-sub">' +
3130
+ (hasFilters
3131
+ ? 'No events match the current filters and time range.'
3132
+ : 'No events have been recorded yet, or none match the selected time range.') +
3133
+ '</div>' +
3134
+ '<ul class="search-no-results-tips">' +
3135
+ '<li>Widen the time range (try <em>All time</em>)</li>' +
3136
+ (hasFilters ? '<li>Remove one or more active filters</li>' : '') +
3137
+ '<li>Make sure the gateway has received at least one tool call</li>' +
3138
+ '<li>Search supports plain text and <code>key=value</code> pairs — pipe syntax is not supported</li>' +
3139
+ '</ul>' +
3140
+ '</div>';
3141
+ tbody.innerHTML = '<tr><td colspan="2">' + emptyHtml + '</td></tr>';
3142
+ document.getElementById('search-pagination').innerHTML = '';
3143
+ var sidebar = document.getElementById('search-fields-sidebar');
3144
+ if (sidebar) sidebar.innerHTML = '';
3145
+ return;
3146
+ }
3147
+
3148
+ tbody.innerHTML = results.map(function(r, i) {
3149
+ var ts = new Date(r.timestamp);
3150
+ var tsStr = ts.toLocaleString('en-US', { month: '2-digit', day: '2-digit', year: '2-digit',
3151
+ hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false });
3152
+ var decValCls = r.decision === 'allow' ? 'kv-allow' : 'kv-deny';
3153
+ var eventLine =
3154
+ '<span class="kv-key">decision=</span><span class="' + decValCls + '">' + (r.decision || '').toUpperCase() + '</span> ' +
3155
+ '<span class="kv-key">actor=</span><span class="kv-actor">' + escapeHtml(r.actor || '—') + '</span> ' +
3156
+ '<span class="kv-key">tool=</span><span class="kv-tool">' + escapeHtml(r.tool || '—') + '</span> ' +
3157
+ '<span class="kv-key">upstream=</span><span class="kv-upstream">' + escapeHtml(r.upstreamId || '—') + '</span>' +
3158
+ (r.latencyMs != null ? ' <span class="kv-key">latency=</span><span style="color:var(--text-secondary);">' + r.latencyMs + 'ms</span>' : '');
3159
+ var isExpanded = expandedRowIdx === i;
3160
+ return '<tr><td colspan="2">' +
3161
+ '<div class="slt-row" onclick="toggleRowExpand(' + i + ')">' +
3162
+ '<span class="slt-chevron' + (isExpanded ? ' open' : '') + '">&#9658;</span>' +
3163
+ '<span class="slt-time">' + escapeHtml(tsStr) + '</span>' +
3164
+ '<span class="slt-event">' + eventLine + '</span>' +
3165
+ '</div>' +
3166
+ (isExpanded ? renderExpansion(r, i) : '') +
3167
+ '</td></tr>';
3168
+ }).join('');
3169
+
3170
+ renderPagination(total);
3171
+ }
3172
+
3173
+ // ── Fields sidebar ──────────────────────────────────────────────────────
3174
+
3175
+ var SFS_SELECTED = [
3176
+ { key: 'actor', label: 'actor' },
3177
+ { key: 'tool', label: 'tool' },
3178
+ { key: 'decision', label: 'decision' },
3179
+ { key: 'upstreamId', label: 'upstream' },
3180
+ ];
3181
+ var SFS_INTERESTING = [
3182
+ { key: 'reason', label: 'reason' },
3183
+ { key: 'outcome', label: 'outcome' },
3184
+ { key: 'latencyMs', label: 'latency_ms' },
3185
+ { key: 'riskScore', label: 'risk_score' },
3186
+ { key: 'sessionId', label: 'session_id' },
3187
+ ];
3188
+
3189
+ function sfsFieldStats(key, results) {
3190
+ var counts = {};
3191
+ results.forEach(function(r) {
3192
+ var v = r[key]; if (v == null || v === '') return;
3193
+ var s = String(v); counts[s] = (counts[s] || 0) + 1;
3194
+ });
3195
+ var arr = Object.keys(counts).map(function(v) { return { value: v, count: counts[v] }; });
3196
+ arr.sort(function(a, b) { return b.count - a.count; });
3197
+ return arr;
3198
+ }
3199
+
3200
+ function renderSfsSection(title, fields, results) {
3201
+ var html = '<div class="sfs-section"><div class="sfs-section-hdr">' + title + '</div>';
3202
+ fields.forEach(function(f) {
3203
+ var stats = sfsFieldStats(f.key, results);
3204
+ if (!stats.length) return;
3205
+ var maxC = stats[0].count;
3206
+ var top = stats.slice(0, 10);
3207
+ var isOpen = !!sfsExpandedFields[f.key];
3208
+ html += '<div class="sfs-field">' +
3209
+ '<div class="sfs-field-hdr" onclick="toggleSfsField(\'' + f.key + '\')">' +
3210
+ '<span class="sfs-field-name">' + escapeHtml(f.label) + '</span>' +
3211
+ '<span class="sfs-field-card">' + stats.length + '</span>' +
3212
+ '<span class="sfs-field-chevron' + (isOpen ? ' open' : '') + '">&#9658;</span>' +
3213
+ '</div>' +
3214
+ '<div class="sfs-field-values' + (isOpen ? ' open' : '') + '" id="sfs-vals-' + f.key + '">' +
3215
+ top.map(function(entry) {
3216
+ var pct = Math.round(entry.count / maxC * 100);
3217
+ var filterKey = f.key === 'upstreamId' ? 'upstream' : f.key;
3218
+ var canFilter = (filterKey !== 'latencyMs' && filterKey !== 'riskScore');
3219
+ return '<div class="sfs-value-row"' +
3220
+ (canFilter ? ' onclick="addSfsFilter(\'' + filterKey + '\',' + JSON.stringify(entry.value) + ')" title="Filter: ' + filterKey + '=\'' + escapeHtml(entry.value) + '\'"' : '') + '>' +
3221
+ '<div class="sfs-value-bar-wrap"><div class="sfs-value-bar" style="width:' + pct + '%"></div></div>' +
3222
+ '<span class="sfs-value-label">' + escapeHtml(entry.value) + '</span>' +
3223
+ '<span class="sfs-value-count">' + entry.count + '</span>' +
3224
+ '</div>';
3225
+ }).join('') +
3226
+ '</div></div>';
3227
+ });
3228
+ html += '</div>';
3229
+ return html;
3230
+ }
3231
+
3232
+ function renderFieldsSidebar(results) {
3233
+ var sidebar = document.getElementById('search-fields-sidebar');
3234
+ if (!sidebar) return;
3235
+ if (!results || !results.length) { sidebar.innerHTML = ''; return; }
3236
+ sidebar.innerHTML =
3237
+ renderSfsSection('Selected Fields', SFS_SELECTED, results) +
3238
+ renderSfsSection('Interesting Fields', SFS_INTERESTING, results);
3239
+ }
3240
+
3241
+ function toggleSfsField(key) {
3242
+ sfsExpandedFields[key] = !sfsExpandedFields[key];
3243
+ var el = document.getElementById('sfs-vals-' + key);
3244
+ if (el) el.classList.toggle('open', !!sfsExpandedFields[key]);
3245
+ var hdrs = document.querySelectorAll('.sfs-field-hdr');
3246
+ hdrs.forEach(function(hdr) {
3247
+ if (hdr.getAttribute('onclick') && hdr.getAttribute('onclick').indexOf('\'' + key + '\'') !== -1) {
3248
+ var chev = hdr.querySelector('.sfs-field-chevron');
3249
+ if (chev) chev.classList.toggle('open', !!sfsExpandedFields[key]);
3250
+ }
3251
+ });
3252
+ }
3253
+
3254
+ function addSfsFilter(key, value) {
3255
+ activeSearchFilters = activeSearchFilters.filter(function(f) { return f.key !== key; });
3256
+ activeSearchFilters.push({ key: key, value: value });
3257
+ renderFilterChips();
3258
+ runSearch(false);
3259
+ }
3260
+
3261
+ function renderPagination(total) {
3262
+ var totalPages = Math.ceil(total / searchPageSize);
3263
+ var pag = document.getElementById('search-pagination');
3264
+ var startRow = searchPage * searchPageSize + 1;
3265
+ var endRow = Math.min((searchPage + 1) * searchPageSize, total);
3266
+
3267
+ var leftHtml =
3268
+ '<div class="search-pagination-left">' +
3269
+ '<span>Rows per page:</span>' +
3270
+ '<select class="search-page-size-select" onchange="changePageSize(this.value)">' +
3271
+ [25, 50, 100, 250].map(function(n) {
3272
+ return '<option value="' + n + '"' + (n === searchPageSize ? ' selected' : '') + '>' + n + '</option>';
3273
+ }).join('') +
3274
+ '</select>' +
3275
+ (total > 0 ? '<span style="margin-left:8px;">' + startRow + '–' + endRow + ' of ' + total.toLocaleString() + '</span>' : '') +
3276
+ '</div>';
3277
+
3278
+ var rightHtml =
3279
+ '<div class="search-pagination-right">' +
3280
+ (totalPages > 1 ? '<button ' + (searchPage === 0 ? 'disabled' : '') + ' onclick="searchGoPage(' + (searchPage-1) + ')">&#8592; Prev</button>' : '') +
3281
+ (totalPages > 1 ? '<span class="page-info">Page ' + (searchPage+1) + ' of ' + totalPages + '</span>' : '') +
3282
+ (totalPages > 1 ? '<button ' + (searchPage >= totalPages-1 ? 'disabled' : '') + ' onclick="searchGoPage(' + (searchPage+1) + ')">Next &#8594;</button>' : '') +
3283
+ '</div>';
3284
+
3285
+ pag.innerHTML = leftHtml + rightHtml;
3286
+ }
3287
+
3288
+ function changePageSize(val) {
3289
+ searchPageSize = parseInt(val, 10) || 50;
3290
+ searchPage = 0;
3291
+ runSearch(false);
3292
+ }
3293
+
3294
+ function renderExpansion(r, i) {
3295
+ var argsContent;
3296
+ if (r.argsJson) {
3297
+ try { argsContent = JSON.stringify(JSON.parse(r.argsJson), null, 2); } catch(e) { argsContent = r.argsJson; }
3298
+ } else { argsContent = r.argsHash ? '(hash) ' + r.argsHash : '—'; }
3299
+
3300
+ var responseContent;
3301
+ if (r.responseJson) {
3302
+ try { responseContent = JSON.stringify(JSON.parse(r.responseJson), null, 2); } catch(e) { responseContent = r.responseJson; }
3303
+ } else if (r.outcome === 'denied') { responseContent = '— denied'; }
3304
+ else if (r.outcome === 'error') { responseContent = '— upstream error'; }
3305
+ else { responseContent = r.responseHash ? '(hash) ' + r.responseHash : '—'; }
3306
+
3307
+ var decValCls = r.decision === 'allow' ? 'kv-allow' : 'kv-deny';
3308
+ var decBadge = '<span class="' + decValCls + '">' + (r.decision || '').toUpperCase() + '</span>';
3309
+
3310
+ // Build field rows: [label, value, filterKey (optional), colorCls]
3311
+ var ts = new Date(r.timestamp);
3312
+ var fullTs = ts.getFullYear() + '-' +
3313
+ String(ts.getMonth()+1).padStart(2,'0') + '-' +
3314
+ String(ts.getDate()).padStart(2,'0') + ' ' +
3315
+ String(ts.getHours()).padStart(2,'0') + ':' +
3316
+ String(ts.getMinutes()).padStart(2,'0') + ':' +
3317
+ String(ts.getSeconds()).padStart(2,'0') + '.' +
3318
+ String(ts.getMilliseconds()).padStart(3,'0');
3319
+
3320
+ var fieldRows = [
3321
+ { lbl: 'time', val: fullTs, cls: 'kv-ts' },
3322
+ { lbl: 'decision', html: decBadge },
3323
+ { lbl: 'actor', val: r.actor, filter: 'actor', cls: 'kv-actor' },
3324
+ { lbl: 'tool', val: r.tool, filter: 'tool', cls: 'kv-tool' },
3325
+ { lbl: 'upstream', val: r.upstreamId, filter: 'upstream', cls: 'kv-upstream' },
3326
+ { lbl: 'reason', val: r.reason, filter: 'reason' },
3327
+ { lbl: 'outcome', val: r.outcome, filter: 'outcome' },
3328
+ { lbl: 'latency', val: r.latencyMs != null ? r.latencyMs + 'ms' : null },
3329
+ { lbl: 'risk_score', val: r.riskScore != null ? String(r.riskScore) : null },
3330
+ { lbl: 'session_id', val: r.sessionId, filter: 'sessionId' },
3331
+ { lbl: 'request_id', val: r.requestId },
3332
+ { lbl: 'ip', val: r.ip },
3333
+ ];
3334
+
3335
+ var fieldTableHtml = fieldRows.map(function(f) {
3336
+ var v = f.html || null;
3337
+ if (!v) {
3338
+ if (!f.val) return '';
3339
+ var valCls = f.cls || '';
3340
+ var innerHtml = '<span class="' + valCls + (f.filter ? ' slt-exp-fval clickable' : ' slt-exp-fval') + '"' +
3341
+ (f.filter ? ' onclick="addSfsFilter(\'' + f.filter + '\',' + JSON.stringify(f.val) + ')" title="Filter on this value"' : '') +
3342
+ '>' + escapeHtml(f.val) + '</span>';
3343
+ v = innerHtml;
3344
+ } else {
3345
+ v = '<span class="slt-exp-fval">' + f.html + '</span>';
3346
+ }
3347
+ return '<div class="slt-exp-field-row">' +
3348
+ '<span class="slt-exp-fname">' + f.lbl + '</span>' + v +
3349
+ '</div>';
3350
+ }).filter(Boolean).join('');
3351
+
3352
+ return '<div class="slt-expansion">' +
3353
+ '<div class="slt-exp-fields">' +
3354
+ fieldTableHtml +
3355
+ '</div>' +
3356
+ '<div class="slt-exp-json-col">' +
3357
+ '<div class="slt-exp-field"><div class="slt-exp-lbl">Request Args</div><div class="slt-exp-code">' + escapeHtml(argsContent) + '</div></div>' +
3358
+ '<div class="slt-exp-field"><div class="slt-exp-lbl">Response</div><div class="slt-exp-code">' + escapeHtml(responseContent) + '</div></div>' +
3359
+ '</div>' +
3360
+ '</div>';
3361
+ }
3362
+
3363
+ function toggleRowExpand(idx) {
3364
+ expandedRowIdx = (expandedRowIdx === idx) ? null : idx;
3365
+ renderSearchResults({ results: currentSearchResults, total: currentSearchTotal });
3366
+ }
3367
+
3368
+ function searchGoPage(page) { searchPage = page; runSearch(false); }
3369
+
3370
+ function stopSearchStreaming() {} // no-op — streaming removed in favour of manual search
3371
+
3372
+ function exportSearchResults() {
3373
+ var params = new URLSearchParams();
3374
+ activeSearchFilters.forEach(function(f) {
3375
+ if (f.key === 'decision') params.set('decision', f.value);
3376
+ else params.set('q', f.value);
3377
+ });
3378
+ params.set('limit', '500'); params.set('offset', '0');
3379
+ fetch(API_BASE + '/api/admin/search?' + params.toString())
3380
+ .then(function(r) { return r.json(); })
3381
+ .then(function(data) {
3382
+ if (!data.results || !data.results.length) { alert('No results to export.'); return; }
3383
+ var headers = ['Timestamp','Actor','OrgId','Tool','Upstream','Decision','LatencyMs'];
3384
+ var rows = data.results.map(function(r) {
3385
+ return [r.timestamp,r.actor,r.orgId,r.tool,r.upstreamId,r.decision,r.latencyMs]
3386
+ .map(function(v) { return '"' + String(v!=null?v:'').replace(/"/g,'""') + '"'; }).join(',');
3387
+ });
3388
+ var csv = [headers.join(',')].concat(rows).join('\n');
3389
+ var blob = new Blob([csv], { type: 'text/csv' });
3390
+ var url = URL.createObjectURL(blob);
3391
+ var a = document.createElement('a'); a.href = url; a.download = 'tool-calls.csv'; a.click();
3392
+ URL.revokeObjectURL(url);
3393
+ });
3394
+ }
3395
+
3396
+ // ── Call Details Panel ──
3397
+ function openCallDetails(r) {
3398
+ activeCallRecord = r;
3399
+ var ts = new Date(r.timestamp);
3400
+ var tsStr = ts.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', year: 'numeric' }) +
3401
+ ', ' + ts.toLocaleTimeString('en-US', { hour12: false });
3402
+ var decValCls = r.decision === 'allow' ? 'kv-allow' : 'kv-deny';
3403
+ var decisionBadge = '<span class="' + decValCls + '">' + (r.decision || '').toUpperCase() + '</span>';
3404
+
3405
+ var argsContent;
3406
+ if (r.argsJson) {
3407
+ try { argsContent = JSON.stringify(JSON.parse(r.argsJson), null, 2); }
3408
+ catch(e) { argsContent = r.argsJson; }
3409
+ } else {
3410
+ argsContent = '(hash) ' + r.argsHash;
3411
+ }
3412
+
3413
+ var responseContent;
3414
+ if (r.responseJson) {
3415
+ try { responseContent = JSON.stringify(JSON.parse(r.responseJson), null, 2); }
3416
+ catch(e) { responseContent = r.responseJson; }
3417
+ } else if (r.outcome === 'denied') {
3418
+ responseContent = '— denied, no upstream call made';
3419
+ } else if (r.outcome === 'error') {
3420
+ responseContent = '— upstream returned an error';
3421
+ } else if (r.responseHash) {
3422
+ responseContent = '(hash) ' + r.responseHash;
3423
+ } else {
3424
+ responseContent = '—';
3425
+ }
3426
+
3427
+ document.getElementById('cdp-body').innerHTML =
3428
+ '<div class="cdp-field">' +
3429
+ '<div class="cdp-label">Timestamp</div>' +
3430
+ '<div class="cdp-value">' + tsStr + '</div>' +
3431
+ '</div>' +
3432
+ '<div class="cdp-field">' +
3433
+ '<div class="cdp-label">Tool</div>' +
3434
+ '<div class="cdp-value cdp-value-mono">' + escapeHtml(r.tool) + '</div>' +
3435
+ '</div>' +
3436
+ '<div class="cdp-field">' +
3437
+ '<div class="cdp-label">Actor</div>' +
3438
+ '<div class="cdp-value">' + escapeHtml(r.actor) + '</div>' +
3439
+ (r.orgId ? '<div class="cdp-value-muted">' + escapeHtml(r.orgId) + '</div>' : '') +
3440
+ '</div>' +
3441
+ '<div class="cdp-field">' +
3442
+ '<div class="cdp-label">Upstream</div>' +
3443
+ '<div class="cdp-value cdp-value-mono">' + escapeHtml(r.upstreamId) + '</div>' +
3444
+ '</div>' +
3445
+ '<div class="cdp-field">' +
3446
+ '<div class="cdp-label">Decision</div>' +
3447
+ '<div class="cdp-value" style="margin-top:2px;">' + decisionBadge + '</div>' +
3448
+ '</div>' +
3449
+ '<div class="cdp-field">' +
3450
+ '<div class="cdp-label">Reason</div>' +
3451
+ '<div class="cdp-value">' + escapeHtml(r.reason) + '</div>' +
3452
+ '</div>' +
3453
+ '<div class="cdp-field">' +
3454
+ '<div class="cdp-label">Latency</div>' +
3455
+ '<div class="cdp-value">' + (r.latencyMs != null ? r.latencyMs + 'ms' : '—') + '</div>' +
3456
+ '</div>' +
3457
+ '<hr class="cdp-divider">' +
3458
+ '<div>' +
3459
+ '<div class="cdp-section-label">Request</div>' +
3460
+ '<div class="cdp-code">' + escapeHtml(argsContent) + '</div>' +
3461
+ '</div>' +
3462
+ '<div>' +
3463
+ '<div class="cdp-section-label">Response</div>' +
3464
+ '<div class="cdp-code">' + escapeHtml(responseContent) + '</div>' +
3465
+ '</div>' +
3466
+ '<hr class="cdp-divider">' +
3467
+ '<div class="cdp-row">' +
3468
+ '<div class="cdp-field">' +
3469
+ '<div class="cdp-label">Outcome</div>' +
3470
+ '<div class="cdp-value">' + escapeHtml(r.outcome) + '</div>' +
3471
+ '</div>' +
3472
+ '<div class="cdp-field">' +
3473
+ '<div class="cdp-label">IP</div>' +
3474
+ '<div class="cdp-value cdp-value-mono">' + escapeHtml(r.ip) + '</div>' +
3475
+ '</div>' +
3476
+ '</div>' +
3477
+ '<div class="cdp-field">' +
3478
+ '<div class="cdp-label">Request ID</div>' +
3479
+ '<div class="cdp-value-muted cdp-value-mono" style="font-size:0.73rem;word-break:break-all;">' + escapeHtml(r.requestId) + '</div>' +
3480
+ '</div>' +
3481
+ '<div class="cdp-field">' +
3482
+ '<div class="cdp-label">Session</div>' +
3483
+ '<div class="cdp-value-muted cdp-value-mono" style="font-size:0.73rem;word-break:break-all;">' + escapeHtml(r.sessionId) + '</div>' +
3484
+ '</div>';
3485
+
3486
+ document.querySelectorAll('.search-table tbody tr.selected').forEach(function(row) { row.classList.remove('selected'); });
3487
+ var row = document.querySelector('.search-table tbody tr[data-idx="' + currentSearchResults.indexOf(r) + '"]');
3488
+ if (row) row.classList.add('selected');
3489
+
3490
+ document.getElementById('call-details-overlay').classList.add('open');
3491
+ document.getElementById('call-details-panel').classList.add('open');
3492
+ }
3493
+
3494
+ function closeCallDetails() {
3495
+ activeCallRecord = null;
3496
+ document.getElementById('call-details-overlay').classList.remove('open');
3497
+ document.getElementById('call-details-panel').classList.remove('open');
3498
+ document.querySelectorAll('.search-table tbody tr.selected').forEach(function(row) { row.classList.remove('selected'); });
3499
+ }
3500
+
3501
+ // ── Page navigation ──
3502
+ var dashboardInterval = null;
3503
+
3504
+ // ── Actors & Roles ──
3505
+ var actorList = [];
3506
+ var selectedActorName = null;
3507
+ var actorMiniChartInstance = null;
3508
+
3509
+ function enrichActor(name, callCount) {
3510
+ var type = /^(agent-|service-|svc-|claude)/.test(name) ? 'Service' : 'User';
3511
+ var roles = [];
3512
+ if (/admin/.test(name)) roles.push('admin');
3513
+ if (/developer|agent|claude/.test(name)) roles.push('developer');
3514
+ if (/auditor|audit/.test(name)) roles.push('auditor');
3515
+ if (/read.?only|readonly/.test(name)) roles.push('read-only');
3516
+ if (!roles.length) roles.push('developer');
3517
+ var toolAccess = roles.indexOf('admin') >= 0 ? 'Full' :
3518
+ roles.indexOf('read-only') >= 0 ? 'Read Only' : 'Limited';
3519
+ return { name: name, type: type, roles: roles, org: 'Oculi Security', toolAccess: toolAccess, callCount: callCount };
3520
+ }
3521
+
3522
+ function loadActors() {
3523
+ document.getElementById('actors-tbody').innerHTML =
3524
+ '<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-muted);">Loading…</td></tr>';
3525
+ fetch(API_BASE + '/api/admin/stats/top-actors?limit=50')
3526
+ .then(function(r) { return r.json(); })
3527
+ .then(function(data) {
3528
+ actorList = (Array.isArray(data) ? data : []).map(function(a) {
3529
+ return enrichActor(a.actor, a.count);
3530
+ });
3531
+ renderActorsTable();
3532
+ })
3533
+ .catch(function() {
3534
+ document.getElementById('actors-tbody').innerHTML =
3535
+ '<tr><td colspan="5" style="text-align:center;padding:40px;color:var(--text-muted);">Failed to load actors.</td></tr>';
3536
+ });
3537
+ }
3538
+
3539
+ function renderActorsTable() {
3540
+ var tbody = document.getElementById('actors-tbody');
3541
+ if (!actorList.length) {
3542
+ tbody.innerHTML =
3543
+ '<tr><td colspan="5" style="text-align:center;padding:48px;color:var(--text-muted);font-size:0.88rem;">' +
3544
+ 'No actors found. Activity will appear here once tool calls are made.</td></tr>';
3545
+ return;
3546
+ }
3547
+ tbody.innerHTML = actorList.map(function(a) {
3548
+ var sel = selectedActorName === a.name ? ' selected' : '';
3549
+ return '<tr class="' + sel + '" onclick="selectActor(\'' + a.name.replace(/'/g, "\\'") + '\')">' +
3550
+ '<td><span class="actor-id-code">' + escapeHtml(a.name) + '</span></td>' +
3551
+ '<td><span class="actor-type-badge">' + a.type + '</span></td>' +
3552
+ '<td>' + a.roles.map(function(r) {
3553
+ return '<span class="role-badge role-' + r + '">' + r + '</span>';
3554
+ }).join('') + '</td>' +
3555
+ '<td>' + escapeHtml(a.org) + '</td>' +
3556
+ '<td>' + escapeHtml(a.toolAccess) + '</td>' +
3557
+ '</tr>';
3558
+ }).join('');
3559
+ }
3560
+
3561
+ function selectActor(name) {
3562
+ selectedActorName = name;
3563
+ renderActorsTable();
3564
+ var actor = null;
3565
+ for (var i = 0; i < actorList.length; i++) {
3566
+ if (actorList[i].name === name) { actor = actorList[i]; break; }
3567
+ }
3568
+ if (!actor) return;
3569
+ document.getElementById('adp-id').textContent = actor.name;
3570
+ document.getElementById('adp-type').innerHTML =
3571
+ '<span class="actor-type-badge">' + actor.type + '</span>';
3572
+ document.getElementById('adp-roles').innerHTML = actor.roles.map(function(r) {
3573
+ return '<span class="role-badge role-' + r + '">' + r + '</span>';
3574
+ }).join(' ');
3575
+ document.getElementById('adp-org').textContent = actor.org;
3576
+ document.getElementById('adp-access').textContent = actor.toolAccess;
3577
+ document.getElementById('adp-jwt').textContent = JSON.stringify({
3578
+ sub: actor.name, org: actor.org, roles: actor.roles
3579
+ }, null, 2);
3580
+ document.getElementById('actor-detail-panel').classList.add('open');
3581
+ loadActorMiniChart();
3582
+ }
3583
+
3584
+ function closeActorPanel() {
3585
+ document.getElementById('actor-detail-panel').classList.remove('open');
3586
+ selectedActorName = null;
3587
+ renderActorsTable();
3588
+ }
3589
+
3590
+ function loadActorMiniChart() {
3591
+ fetch(API_BASE + '/api/admin/stats/timeseries?hours=24&bucket=60')
3592
+ .then(function(r) { return r.json(); })
3593
+ .then(function(buckets) { renderActorMiniChart(buckets); })
3594
+ .catch(function() {});
3595
+ }
3596
+
3597
+ function renderActorMiniChart(buckets) {
3598
+ var canvas = document.getElementById('actor-mini-chart');
3599
+ if (!canvas || !canvas.getContext) return;
3600
+ if (actorMiniChartInstance) { actorMiniChartInstance.destroy(); actorMiniChartInstance = null; }
3601
+ var labels = buckets.map(function(b) {
3602
+ var d = new Date(b.bucket);
3603
+ return String(d.getHours()).padStart(2, '0') + ':' + String(d.getMinutes()).padStart(2, '0');
3604
+ });
3605
+ var vals = buckets.map(function(b) { return b.count; });
3606
+ actorMiniChartInstance = new Chart(canvas, {
3607
+ type: 'line',
3608
+ data: {
3609
+ labels: labels,
3610
+ datasets: [{
3611
+ data: vals, borderColor: '#48bb78', backgroundColor: 'rgba(72,187,120,0.1)',
3612
+ tension: 0.35, borderWidth: 1.5, pointRadius: 2, fill: true
3613
+ }]
3614
+ },
3615
+ options: {
3616
+ responsive: true, maintainAspectRatio: false,
3617
+ plugins: { legend: { display: false } },
3618
+ scales: {
3619
+ x: { ticks: { color: '#718096', font: { size: 9 }, maxTicksLimit: 6 }, grid: { color: 'rgba(255,255,255,0.04)' } },
3620
+ y: { ticks: { color: '#718096', font: { size: 9 } }, grid: { color: 'rgba(255,255,255,0.04)' }, beginAtZero: true }
3621
+ }
3622
+ }
3623
+ });
3624
+ }
3625
+
3626
+ // ── Integrations ──
3627
+ var integrationStats = {}; // cached per-upstream stats keyed by upstreamId
3628
+
3629
+ function copySnippet(btn, text) {
3630
+ navigator.clipboard.writeText(text).then(function() {
3631
+ btn.textContent = 'Copied!';
3632
+ setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
3633
+ }).catch(function() {
3634
+ btn.textContent = 'Failed';
3635
+ setTimeout(function() { btn.textContent = 'Copy'; }, 2000);
3636
+ });
3637
+ }
3638
+
3639
+ function renderAiClients() {
3640
+ var origin = window.location.origin;
3641
+ var grid = document.getElementById('ai-clients-grid');
3642
+ if (!grid) return;
3643
+ var clients = [
3644
+ {
3645
+ cls: 'mcp',
3646
+ iconSvg: '<polyline points="22 12 18 12 15 21 9 3 6 12 2 12"/>',
3647
+ name: 'MCP Server',
3648
+ compat: 'Claude Code · Claude Desktop · any MCP client',
3649
+ desc: 'Connect AI clients via the Model Context Protocol. Supports streamable HTTP and SSE. Tools are discovered automatically; every call passes through the policy pipeline.',
3650
+ endpoints: [
3651
+ { method: 'GET', path: '/mcp', note: 'SSE channel' },
3652
+ { method: 'POST', path: '/mcp', note: 'Streamable HTTP' }
3653
+ ],
3654
+ snippetLabel: 'Claude Code CLI',
3655
+ snippet: 'claude mcp add gateway ' + origin + '/mcp \\\n --header "Authorization: Bearer $GATEWAY_TOKEN"'
3656
+ },
3657
+ {
3658
+ cls: 'openai',
3659
+ iconSvg: '<circle cx="12" cy="12" r="3"/><line x1="12" y1="1" x2="12" y2="5"/><line x1="12" y1="19" x2="12" y2="23"/><line x1="4.22" y1="4.22" x2="7.05" y2="7.05"/><line x1="16.95" y1="16.95" x2="19.78" y2="19.78"/><line x1="1" y1="12" x2="5" y2="12"/><line x1="19" y1="12" x2="23" y2="12"/><line x1="4.22" y1="19.78" x2="7.05" y2="16.95"/><line x1="16.95" y1="7.05" x2="19.78" y2="4.22"/>',
3660
+ name: 'OpenAI Function Calling',
3661
+ compat: 'Open WebUI · LiteLLM · any OpenAI-compatible client',
3662
+ desc: 'Drop-in OpenAI tool_calls format. List available tools and execute them using the standard OpenAI function calling interface. Always returns HTTP 200; per-tool errors are in the response content.',
3663
+ endpoints: [
3664
+ { method: 'GET', path: '/v1/openai/tools', note: 'List all tools' },
3665
+ { method: 'POST', path: '/v1/openai/tools', note: 'Execute tool_calls' }
3666
+ ],
3667
+ snippetLabel: 'Open WebUI → Admin → Tools',
3668
+ snippet: '# Custom Tools Endpoint\nURL: ' + origin + '/v1/openai/tools\nHeader: Authorization: Bearer $GATEWAY_TOKEN'
3669
+ },
3670
+ {
3671
+ cls: 'plugin',
3672
+ iconSvg: '<path d="M20.24 12.24a6 6 0 0 0-8.49-8.49L5 10.5V19h8.5z"/><line x1="16" y1="8" x2="2" y2="22"/><line x1="17.5" y1="15" x2="9" y2="15"/>',
3673
+ name: 'OpenClaw Plugin',
3674
+ compat: 'OpenClaw v0.x (homelab)',
3675
+ desc: 'Install the bundled plugin package to enforce policies on every OpenClaw agent tool call. No build step needed — loaded directly from TypeScript via jiti.',
3676
+ endpoints: [
3677
+ { method: 'GET', path: '/v1/openai/tools', note: 'Tool discovery' },
3678
+ { method: 'POST', path: '/v1/call', note: 'Policy-enforced execution' }
3679
+ ],
3680
+ snippetLabel: 'Environment variables',
3681
+ snippet: 'GATEWAY_URL=' + origin + '\nGATEWAY_ACTOR=openclaw\nGATEWAY_ORG=my-org\nGATEWAY_ROLES=editor\nGATEWAY_JWT_SECRET=<your-jwt-secret>\n# Plugin: apps/openclaw-plugin/src/index.ts'
3682
+ }
3683
+ ];
3684
+
3685
+ grid.innerHTML = clients.map(function(c) {
3686
+ var snippetEscaped = escapeHtml(c.snippet);
3687
+ var snippetJson = JSON.stringify(c.snippet);
3688
+ return '<div class="ai-client-card">' +
3689
+ '<div class="ai-client-header">' +
3690
+ '<div class="ai-client-name-row">' +
3691
+ '<div class="ai-client-icon-name">' +
3692
+ '<div class="ai-client-icon ' + c.cls + '">' +
3693
+ '<svg viewBox="0 0 24 24">' + c.iconSvg + '</svg>' +
3694
+ '</div>' +
3695
+ '<span class="ai-client-name">' + escapeHtml(c.name) + '</span>' +
3696
+ '</div>' +
3697
+ '<div class="ai-client-compat">' + escapeHtml(c.compat) + '</div>' +
3698
+ '</div>' +
3699
+ '</div>' +
3700
+ '<p class="ai-client-desc">' + escapeHtml(c.desc) + '</p>' +
3701
+ '<div class="ai-client-endpoints">' +
3702
+ c.endpoints.map(function(e) {
3703
+ return '<div class="ai-client-endpoint">' +
3704
+ '<span class="ai-endpoint-method ' + e.method.toLowerCase() + '">' + e.method + '</span>' +
3705
+ '<code class="ai-endpoint-path">' + escapeHtml(origin + e.path) + '</code>' +
3706
+ '</div>';
3707
+ }).join('') +
3708
+ '</div>' +
3709
+ '<details class="ai-client-setup">' +
3710
+ '<summary>Setup Instructions</summary>' +
3711
+ '<div class="ai-client-snippet-header">' +
3712
+ '<span class="ai-client-snippet-lbl">' + escapeHtml(c.snippetLabel) + '</span>' +
3713
+ '<button class="btn-copy-snippet" onclick="copySnippet(this,' + snippetJson + ')">Copy</button>' +
3714
+ '</div>' +
3715
+ '<pre class="ai-client-snippet">' + snippetEscaped + '</pre>' +
3716
+ '</details>' +
3717
+ '</div>';
3718
+ }).join('');
3719
+ }
3720
+
3721
+ function loadIntegrations() {
3722
+ document.getElementById('integrations-grid').innerHTML =
3723
+ '<div style="grid-column:1/-1;color:var(--text-muted);padding:24px 0;">Loading…</div>';
3724
+ renderAiClients();
3725
+ // Load settings for upstream config + search stats for metrics in parallel
3726
+ Promise.all([
3727
+ fetch(API_BASE + '/api/admin/settings').then(function(r) { return r.json(); }),
3728
+ fetch(API_BASE + '/api/admin/search?limit=500').then(function(r) { return r.json(); })
3729
+ ]).then(function(results) {
3730
+ var settings = results[0];
3731
+ var searchData = results[1];
3732
+ // Build per-upstream stats from search results
3733
+ integrationStats = {};
3734
+ (searchData.results || []).forEach(function(row) {
3735
+ var uid = row.upstreamId;
3736
+ if (!integrationStats[uid]) integrationStats[uid] = { total: 0, denied: 0, latencies: [] };
3737
+ integrationStats[uid].total++;
3738
+ if (row.decision === 'deny') integrationStats[uid].denied++;
3739
+ if (row.latencyMs != null) integrationStats[uid].latencies.push(row.latencyMs);
3740
+ });
3741
+ renderIntegrations(settings.upstreams || []);
3742
+ }).catch(function() {
3743
+ document.getElementById('integrations-grid').innerHTML =
3744
+ '<div style="grid-column:1/-1;color:var(--text-muted);padding:24px 0;">Failed to load integrations.</div>';
3745
+ });
3746
+ }
3747
+
3748
+ function renderIntegrations(upstreams) {
3749
+ var grid = document.getElementById('integrations-grid');
3750
+ if (!upstreams.length) {
3751
+ grid.innerHTML =
3752
+ '<div style="grid-column:1/-1;color:var(--text-muted);padding:24px 0;">No upstream servers configured. Add entries to <code>config/upstreams.yaml</code>.</div>';
3753
+ return;
3754
+ }
3755
+ grid.innerHTML = upstreams.map(function(u) { return renderIntegrationCard(u); }).join('');
3756
+ }
3757
+
3758
+ function renderIntegrationCard(u) {
3759
+ var name = u.name || u.id;
3760
+ var stats = integrationStats['ext:' + u.id] || integrationStats[u.id] || { total: 0, denied: 0, latencies: [] };
3761
+ var avgLatency = stats.latencies.length
3762
+ ? Math.round(stats.latencies.reduce(function(s, v) { return s + v; }, 0) / stats.latencies.length)
3763
+ : null;
3764
+ var successRate = stats.total > 0
3765
+ ? ((stats.total - stats.denied) / stats.total * 100).toFixed(1)
3766
+ : null;
3767
+ var healthIcon = '<svg viewBox="0 0 24 24" style="width:18px;height:18px;stroke:#48bb78;fill:none;stroke-width:2.5;stroke-linecap:round;stroke-linejoin:round;"><polyline points="20 6 9 17 4 12"/></svg>';
3768
+
3769
+ return '<div class="integration-card">' +
3770
+ '<div class="integration-card-header">' +
3771
+ '<div class="integration-card-name-row">' +
3772
+ healthIcon +
3773
+ '<span class="integration-card-name">' + escapeHtml(name) + '</span>' +
3774
+ '</div>' +
3775
+ '<span class="integration-status-badge active">Active</span>' +
3776
+ '</div>' +
3777
+ '<div class="integration-meta-row">' +
3778
+ '<div><div class="integration-meta-lbl">Latency</div>' +
3779
+ '<div class="integration-meta-val">' + (avgLatency !== null ? avgLatency + 'ms' : 'Timeout: ' + u.timeout + 'ms') + '</div></div>' +
3780
+ '<div><div class="integration-meta-lbl">Upstream ID</div>' +
3781
+ '<div class="integration-meta-val"><code>' + escapeHtml(u.id) + '</code></div></div>' +
3782
+ '</div>' +
3783
+ '<div class="integration-flag-row ' + (u.hasAuth ? 'auth-ok' : 'auth-no') + '">' +
3784
+ '<svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>' +
3785
+ (u.hasAuth ? 'Auth Configured' : 'No Auth') +
3786
+ '</div>' +
3787
+ (stats.total > 0
3788
+ ? '<div class="integration-metrics-row">Success Rate: <strong>' + successRate + '%</strong> &nbsp; Denied: <span class="denied-count">' + stats.denied + '</span></div>'
3789
+ : '<div class="integration-metrics-row" style="color:var(--text-muted);">No traffic recorded yet</div>'
3790
+ ) +
3791
+ '<button class="btn-view-integration" onclick="viewIntegrationDetails(\'' + escapeHtml(u.id) + '\')">View Details</button>' +
3792
+ '</div>';
3793
+ }
3794
+
3795
+ function viewIntegrationDetails(id) {
3796
+ activeSearchFilters = [{ key: 'upstream', value: id }];
3797
+ kqlParams = {};
3798
+ renderFilterChips();
3799
+ document.querySelectorAll('.sidebar-nav a').forEach(function(l) { l.classList.remove('active'); });
3800
+ var searchLink = document.querySelector('.sidebar-nav a[data-page="search"]');
3801
+ if (searchLink) searchLink.classList.add('active');
3802
+ navigateTo('search');
3803
+ }
3804
+
3805
+ var _navFromPopstate = false;
3806
+
3807
+ function navigateTo(pageId) {
3808
+ document.querySelectorAll('.page').forEach(function(p) { p.classList.remove('active'); });
3809
+ var target = document.getElementById('page-' + pageId);
3810
+ if (target) target.classList.add('active');
3811
+
3812
+ // Update URL for deep-linking
3813
+ if (!_navFromPopstate) {
3814
+ history.pushState(null, '', '/admin/' + pageId);
3815
+ }
3816
+
3817
+ // Load data for the target page
3818
+ if (pageId === 'overview') {
3819
+ loadDashboard();
3820
+ if (!dashboardInterval) dashboardInterval = setInterval(loadDashboard, 30000);
3821
+ } else {
3822
+ if (dashboardInterval) { clearInterval(dashboardInterval); dashboardInterval = null; }
3823
+ }
3824
+ if (pageId === 'policies') {
3825
+ if (activePolicyTab === 'visual') loadVisualRules();
3826
+ else loadPolicies();
3827
+ }
3828
+ if (pageId === 'search') loadSearch();
3829
+ if (pageId === 'actors') loadActors();
3830
+ if (pageId !== 'search') { stopSearchStreaming(); }
3831
+ // Close actor panel when navigating away
3832
+ if (pageId !== 'actors') closeActorPanel();
3833
+ }
3834
+
3835
+ function getPageFromPath() {
3836
+ var path = window.location.pathname.replace(/^\/admin\/?/, '').replace(/\/$/, '');
3837
+ if (!path || path === 'index.html') return 'overview';
3838
+ return path;
3839
+ }
3840
+
3841
+ function handleRoute() {
3842
+ var pageId = getPageFromPath();
3843
+ // Unknown page (e.g. stale /admin/settings bookmark) → fall back to overview
3844
+ if (!document.getElementById('page-' + pageId)) {
3845
+ history.replaceState(null, '', '/admin/overview');
3846
+ pageId = 'overview';
3847
+ }
3848
+ document.querySelectorAll('.sidebar-nav a').forEach(function(l) { l.classList.remove('active'); });
3849
+ var link = document.querySelector('.sidebar-nav a[data-page="' + pageId + '"]');
3850
+ if (link) link.classList.add('active');
3851
+ _navFromPopstate = true;
3852
+ navigateTo(pageId);
3853
+ _navFromPopstate = false;
3854
+ }
3855
+
3856
+ window.addEventListener('popstate', handleRoute);
3857
+
3858
+ // ── Sidebar collapse ──
3859
+ function toggleSidebar() {
3860
+ var sidebar = document.getElementById('sidebar');
3861
+ var main = document.querySelector('.main-wrapper');
3862
+ sidebar.classList.toggle('collapsed');
3863
+ main.classList.toggle('collapsed');
3864
+ }
3865
+
3866
+ // ── Init ──
3867
+ document.getElementById('refresh-btn').addEventListener('click', loadDashboard);
3868
+
3869
+ // Route from URL path, default to overview
3870
+ var initPage = getPageFromPath();
3871
+ if (initPage !== 'overview' && document.getElementById('page-' + initPage)) {
3872
+ handleRoute();
3873
+ } else {
3874
+ if (initPage !== 'overview') history.replaceState(null, '', '/admin/overview');
3875
+ loadDashboard();
3876
+ dashboardInterval = setInterval(loadDashboard, 30000);
3877
+ }
3878
+
3879
+
3880
+ // ── Sidebar nav ──
3881
+ document.querySelectorAll('.sidebar-nav a').forEach(function(link) {
3882
+ link.addEventListener('click', function(e) {
3883
+ e.preventDefault();
3884
+ document.querySelectorAll('.sidebar-nav a').forEach(function(l) { l.classList.remove('active'); });
3885
+ link.classList.add('active');
3886
+ var pageId = link.getAttribute('data-page');
3887
+ if (pageId) navigateTo(pageId);
3888
+ });
3889
+ });
3890
+
3891
+ </script>
3892
+ </body>
3893
+ </html>