bashbros 0.1.1 → 0.1.3

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.
@@ -4,407 +4,2004 @@
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>BashBros Dashboard</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
7
10
  <style>
8
- * {
9
- margin: 0;
10
- padding: 0;
11
- box-sizing: border-box;
11
+ :root {
12
+ --white: #FFFFFF;
13
+ --grey-50: #FAFAFA;
14
+ --grey-100: #F5F5F5;
15
+ --grey-200: #EEEEEE;
16
+ --grey-300: #E0E0E0;
17
+ --grey-400: #BDBDBD;
18
+ --grey-500: #9E9E9E;
19
+ --grey-600: #757575;
20
+ --grey-700: #616161;
21
+ --grey-800: #424242;
22
+ --grey-900: #212121;
23
+ --teal: #4DB6AC;
24
+ --teal-light: #80CBC4;
25
+ --teal-dark: #00897B;
26
+ --red: #ef4444;
27
+ --yellow: #fbbf24;
28
+ --orange: #f97316;
29
+ --green: #22c55e;
30
+ --blue: #3b82f6;
31
+ --purple: #8b5cf6;
32
+ --border: 3px solid var(--grey-900);
33
+ --shadow: 6px 6px 0px var(--grey-900);
34
+ --shadow-sm: 4px 4px 0px var(--grey-900);
12
35
  }
13
36
 
37
+ * { margin: 0; padding: 0; box-sizing: border-box; }
38
+
14
39
  body {
15
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
16
- background: #1a1a2e;
17
- color: #eaeaea;
40
+ font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
41
+ background: var(--grey-100);
42
+ color: var(--grey-900);
43
+ line-height: 1.6;
18
44
  min-height: 100vh;
19
- padding: 20px;
20
45
  }
21
46
 
47
+ /* Header */
22
48
  .header {
49
+ background: var(--white);
50
+ border-bottom: var(--border);
51
+ padding: 16px 24px;
23
52
  display: flex;
24
53
  justify-content: space-between;
25
54
  align-items: center;
26
- margin-bottom: 30px;
27
- padding-bottom: 20px;
28
- border-bottom: 1px solid #2d2d44;
55
+ position: sticky;
56
+ top: 0;
57
+ z-index: 100;
29
58
  }
30
59
 
31
- .header h1 {
32
- font-size: 24px;
33
- font-weight: 600;
34
- color: #fff;
60
+ .logo {
61
+ font-family: 'JetBrains Mono', monospace;
62
+ font-weight: 700;
63
+ font-size: 1.4rem;
64
+ display: flex;
65
+ align-items: center;
66
+ gap: 8px;
67
+ }
68
+
69
+ .logo-slash { color: var(--teal); }
70
+
71
+ .header-right {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 20px;
35
75
  }
36
76
 
37
77
  .connection-status {
38
78
  display: flex;
39
79
  align-items: center;
40
80
  gap: 8px;
41
- font-size: 14px;
42
- color: #888;
81
+ font-size: 0.9rem;
82
+ color: var(--grey-600);
43
83
  }
44
84
 
45
85
  .status-dot {
46
86
  width: 10px;
47
87
  height: 10px;
48
88
  border-radius: 50%;
49
- background: #666;
89
+ background: var(--grey-400);
90
+ }
91
+ .status-dot.connected { background: var(--green); }
92
+ .status-dot.disconnected { background: var(--red); }
93
+
94
+ /* Navigation */
95
+ .nav-tabs {
96
+ background: var(--white);
97
+ border-bottom: var(--border);
98
+ display: flex;
99
+ gap: 0;
100
+ overflow-x: auto;
101
+ }
102
+
103
+ .nav-tab {
104
+ padding: 16px 28px;
105
+ font-family: 'JetBrains Mono', monospace;
106
+ font-weight: 600;
107
+ font-size: 0.9rem;
108
+ border: none;
109
+ background: transparent;
110
+ cursor: pointer;
111
+ border-right: 2px solid var(--grey-300);
112
+ transition: all 0.15s ease;
113
+ white-space: nowrap;
114
+ display: flex;
115
+ align-items: center;
116
+ gap: 8px;
117
+ }
118
+
119
+ .nav-tab:hover { background: var(--grey-100); }
120
+ .nav-tab.active {
121
+ background: var(--teal);
122
+ color: var(--white);
123
+ }
124
+
125
+ .nav-badge {
126
+ background: var(--red);
127
+ color: var(--white);
128
+ font-size: 0.7rem;
129
+ padding: 2px 6px;
130
+ border-radius: 10px;
131
+ font-weight: 700;
132
+ }
133
+
134
+ .nav-tab.active .nav-badge {
135
+ background: var(--white);
136
+ color: var(--teal-dark);
137
+ }
138
+
139
+ /* Main Content */
140
+ .main { padding: 24px; max-width: 1600px; margin: 0 auto; }
141
+
142
+ .tab-content { display: none; }
143
+ .tab-content.active { display: block; }
144
+
145
+ /* Cards */
146
+ .card {
147
+ background: var(--white);
148
+ border: var(--border);
149
+ box-shadow: var(--shadow-sm);
150
+ margin-bottom: 24px;
151
+ }
152
+
153
+ .card-header {
154
+ background: var(--grey-200);
155
+ padding: 16px 20px;
156
+ border-bottom: 2px solid var(--grey-900);
157
+ font-weight: 700;
158
+ font-size: 1rem;
159
+ display: flex;
160
+ justify-content: space-between;
161
+ align-items: center;
162
+ }
163
+
164
+ .card-body { padding: 20px; }
165
+ .card-body.no-padding { padding: 0; }
166
+
167
+ /* Grid Layouts */
168
+ .grid-2 {
169
+ display: grid;
170
+ grid-template-columns: repeat(2, 1fr);
171
+ gap: 24px;
172
+ }
173
+
174
+ .grid-3 {
175
+ display: grid;
176
+ grid-template-columns: repeat(3, 1fr);
177
+ gap: 24px;
178
+ }
179
+
180
+ .grid-4 {
181
+ display: grid;
182
+ grid-template-columns: repeat(4, 1fr);
183
+ gap: 20px;
50
184
  }
51
185
 
52
- .status-dot.connected {
53
- background: #4ade80;
186
+ @media (max-width: 1200px) {
187
+ .grid-4 { grid-template-columns: repeat(2, 1fr); }
188
+ .grid-3 { grid-template-columns: repeat(2, 1fr); }
54
189
  }
55
190
 
56
- .status-dot.disconnected {
57
- background: #ef4444;
191
+ @media (max-width: 768px) {
192
+ .grid-4, .grid-3, .grid-2 { grid-template-columns: 1fr; }
58
193
  }
59
194
 
195
+ /* Stats Grid */
60
196
  .stats-grid {
61
197
  display: grid;
62
- grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
198
+ grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
63
199
  gap: 20px;
64
- margin-bottom: 30px;
200
+ margin-bottom: 24px;
65
201
  }
66
202
 
67
203
  .stat-card {
68
- background: #16213e;
69
- border-radius: 12px;
204
+ background: var(--white);
205
+ border: var(--border);
206
+ box-shadow: var(--shadow-sm);
70
207
  padding: 20px;
71
- border: 1px solid #2d2d44;
72
208
  }
73
209
 
74
- .stat-card .label {
75
- font-size: 14px;
76
- color: #888;
210
+ .stat-label {
211
+ font-size: 0.85rem;
212
+ color: var(--grey-600);
77
213
  margin-bottom: 8px;
214
+ font-weight: 500;
78
215
  }
79
216
 
80
- .stat-card .value {
81
- font-size: 32px;
217
+ .stat-value {
218
+ font-family: 'JetBrains Mono', monospace;
219
+ font-size: 2rem;
82
220
  font-weight: 700;
83
- color: #fff;
84
221
  }
85
222
 
86
- .stat-card.warning .value {
87
- color: #fbbf24;
223
+ .stat-value.warning { color: var(--yellow); }
224
+ .stat-value.error { color: var(--red); }
225
+ .stat-value.success { color: var(--green); }
226
+ .stat-value.info { color: var(--blue); }
227
+
228
+ /* Session Banner */
229
+ .session-banner {
230
+ background: var(--teal-light);
231
+ border: var(--border);
232
+ box-shadow: var(--shadow-sm);
233
+ padding: 16px 24px;
234
+ margin-bottom: 24px;
235
+ display: flex;
236
+ justify-content: space-between;
237
+ align-items: center;
238
+ flex-wrap: wrap;
239
+ gap: 16px;
88
240
  }
89
241
 
90
- .stat-card.error .value {
91
- color: #ef4444;
242
+ .session-banner.inactive {
243
+ background: var(--grey-200);
92
244
  }
93
245
 
94
- .stat-card.success .value {
95
- color: #4ade80;
246
+ .session-info {
247
+ display: flex;
248
+ gap: 32px;
249
+ flex-wrap: wrap;
96
250
  }
97
251
 
98
- .section {
99
- background: #16213e;
100
- border-radius: 12px;
101
- padding: 20px;
102
- border: 1px solid #2d2d44;
103
- margin-bottom: 20px;
252
+ .session-stat {
253
+ display: flex;
254
+ flex-direction: column;
104
255
  }
105
256
 
106
- .section h2 {
107
- font-size: 18px;
257
+ .session-stat-label {
258
+ font-size: 0.75rem;
259
+ color: var(--grey-700);
260
+ text-transform: uppercase;
108
261
  font-weight: 600;
109
- margin-bottom: 16px;
110
- color: #fff;
111
262
  }
112
263
 
113
- .activity-list {
114
- list-style: none;
264
+ .session-stat-value {
265
+ font-family: 'JetBrains Mono', monospace;
266
+ font-size: 1.1rem;
267
+ font-weight: 700;
268
+ }
269
+
270
+ /* Live Feed */
271
+ .live-feed {
272
+ max-height: 500px;
273
+ overflow-y: auto;
115
274
  }
116
275
 
117
- .activity-item {
276
+ .command-item {
118
277
  display: flex;
119
278
  align-items: flex-start;
120
279
  gap: 12px;
121
- padding: 12px 0;
122
- border-bottom: 1px solid #2d2d44;
280
+ padding: 12px 16px;
281
+ border-bottom: 1px solid var(--grey-200);
282
+ transition: background 0.15s;
123
283
  }
124
284
 
125
- .activity-item:last-child {
126
- border-bottom: none;
285
+ .command-item:hover {
286
+ background: var(--grey-50);
127
287
  }
128
288
 
129
- .activity-icon {
130
- width: 32px;
131
- height: 32px;
132
- border-radius: 8px;
289
+ .command-item:last-child { border-bottom: none; }
290
+
291
+ .command-status {
292
+ width: 8px;
293
+ height: 8px;
294
+ border-radius: 50%;
295
+ margin-top: 8px;
296
+ flex-shrink: 0;
297
+ }
298
+
299
+ .command-status.allowed { background: var(--green); }
300
+ .command-status.blocked { background: var(--red); }
301
+
302
+ .command-content { flex: 1; min-width: 0; }
303
+
304
+ .command-text {
305
+ font-family: 'JetBrains Mono', monospace;
306
+ font-size: 0.9rem;
307
+ word-break: break-all;
308
+ }
309
+
310
+ .command-meta {
311
+ display: flex;
312
+ gap: 16px;
313
+ margin-top: 4px;
314
+ font-size: 0.75rem;
315
+ color: var(--grey-500);
316
+ }
317
+
318
+ .risk-badge {
319
+ font-family: 'JetBrains Mono', monospace;
320
+ font-size: 0.7rem;
321
+ font-weight: 700;
322
+ padding: 2px 8px;
323
+ border-radius: 4px;
324
+ text-transform: uppercase;
325
+ }
326
+
327
+ .risk-badge.safe { background: #dcfce7; color: #166534; }
328
+ .risk-badge.caution { background: #fef9c3; color: #854d0e; }
329
+ .risk-badge.dangerous { background: #fed7aa; color: #c2410c; }
330
+ .risk-badge.critical { background: #fecaca; color: #991b1b; }
331
+
332
+ /* Form Elements */
333
+ .form-group { margin-bottom: 20px; }
334
+
335
+ .form-label {
336
+ display: block;
337
+ font-weight: 600;
338
+ margin-bottom: 8px;
339
+ font-size: 0.9rem;
340
+ }
341
+
342
+ .form-select, .form-input {
343
+ width: 100%;
344
+ padding: 12px 16px;
345
+ font-family: 'JetBrains Mono', monospace;
346
+ font-size: 0.9rem;
347
+ border: 2px solid var(--grey-900);
348
+ background: var(--white);
349
+ transition: all 0.15s ease;
350
+ }
351
+
352
+ .form-select:focus, .form-input:focus {
353
+ outline: none;
354
+ box-shadow: 4px 4px 0px var(--teal);
355
+ }
356
+
357
+ /* Toggle Switch */
358
+ .toggle-row {
133
359
  display: flex;
360
+ justify-content: space-between;
134
361
  align-items: center;
135
- justify-content: center;
136
- font-size: 14px;
137
- flex-shrink: 0;
362
+ padding: 12px 0;
363
+ border-bottom: 1px solid var(--grey-200);
138
364
  }
139
365
 
140
- .activity-icon.info {
141
- background: #1e40af;
142
- color: #60a5fa;
366
+ .toggle-row:last-child { border-bottom: none; }
367
+
368
+ .toggle-info h4 {
369
+ font-size: 0.95rem;
370
+ font-weight: 600;
371
+ margin-bottom: 2px;
143
372
  }
144
373
 
145
- .activity-icon.warning {
146
- background: #854d0e;
147
- color: #fbbf24;
374
+ .toggle-info p {
375
+ font-size: 0.85rem;
376
+ color: var(--grey-600);
148
377
  }
149
378
 
150
- .activity-icon.error {
151
- background: #7f1d1d;
152
- color: #ef4444;
379
+ .toggle {
380
+ position: relative;
381
+ width: 52px;
382
+ height: 28px;
383
+ cursor: pointer;
153
384
  }
154
385
 
155
- .activity-icon.debug {
156
- background: #374151;
157
- color: #9ca3af;
386
+ .toggle input {
387
+ opacity: 0;
388
+ width: 0;
389
+ height: 0;
158
390
  }
159
391
 
160
- .activity-content {
161
- flex: 1;
162
- min-width: 0;
392
+ .toggle-slider {
393
+ position: absolute;
394
+ inset: 0;
395
+ background: var(--grey-300);
396
+ border: 2px solid var(--grey-900);
397
+ transition: 0.2s;
163
398
  }
164
399
 
165
- .activity-message {
166
- font-size: 14px;
167
- color: #eaeaea;
168
- word-break: break-word;
400
+ .toggle-slider:before {
401
+ content: "";
402
+ position: absolute;
403
+ height: 18px;
404
+ width: 18px;
405
+ left: 3px;
406
+ bottom: 3px;
407
+ background: var(--grey-900);
408
+ transition: 0.2s;
169
409
  }
170
410
 
171
- .activity-meta {
411
+ .toggle input:checked + .toggle-slider {
412
+ background: var(--teal);
413
+ }
414
+
415
+ .toggle input:checked + .toggle-slider:before {
416
+ transform: translateX(24px);
417
+ background: var(--white);
418
+ }
419
+
420
+ /* List Editor */
421
+ .list-editor {
422
+ border: 2px solid var(--grey-300);
423
+ max-height: 300px;
424
+ overflow-y: auto;
425
+ }
426
+
427
+ .list-item {
172
428
  display: flex;
173
- gap: 16px;
174
- margin-top: 4px;
175
- font-size: 12px;
176
- color: #666;
429
+ justify-content: space-between;
430
+ align-items: center;
431
+ padding: 10px 16px;
432
+ border-bottom: 1px solid var(--grey-200);
433
+ font-family: 'JetBrains Mono', monospace;
434
+ font-size: 0.85rem;
177
435
  }
178
436
 
179
- .activity-meta span {
437
+ .list-item:last-child { border-bottom: none; }
438
+ .list-item:nth-child(even) { background: var(--grey-50); }
439
+
440
+ .list-item-text {
441
+ flex: 1;
442
+ word-break: break-all;
443
+ }
444
+
445
+ .list-item-remove {
446
+ background: none;
447
+ border: none;
448
+ color: var(--red);
449
+ cursor: pointer;
450
+ font-size: 1.2rem;
451
+ padding: 0 8px;
452
+ font-weight: bold;
453
+ }
454
+
455
+ .list-add {
180
456
  display: flex;
457
+ gap: 8px;
458
+ margin-top: 12px;
459
+ }
460
+
461
+ .list-add input { flex: 1; }
462
+
463
+ /* Buttons */
464
+ .btn {
465
+ display: inline-flex;
181
466
  align-items: center;
182
- gap: 4px;
467
+ gap: 8px;
468
+ padding: 12px 24px;
469
+ font-family: 'JetBrains Mono', monospace;
470
+ font-weight: 600;
471
+ font-size: 0.9rem;
472
+ border: var(--border);
473
+ background: var(--white);
474
+ color: var(--grey-900);
475
+ cursor: pointer;
476
+ transition: all 0.15s ease;
477
+ box-shadow: var(--shadow-sm);
183
478
  }
184
479
 
185
- .empty-state {
186
- text-align: center;
187
- padding: 40px 20px;
188
- color: #666;
480
+ .btn:hover {
481
+ transform: translate(2px, 2px);
482
+ box-shadow: 2px 2px 0px var(--grey-900);
189
483
  }
190
484
 
191
- .empty-state p {
192
- font-size: 14px;
485
+ .btn-primary {
486
+ background: var(--teal);
487
+ color: var(--white);
193
488
  }
194
- </style>
195
- </head>
196
- <body>
197
- <div class="header">
198
- <h1>BashBros Dashboard</h1>
199
- <div class="connection-status">
200
- <span class="status-dot" id="connectionDot"></span>
201
- <span id="connectionText">Connecting...</span>
202
- </div>
203
- </div>
204
489
 
205
- <div class="stats-grid">
206
- <div class="stat-card">
207
- <div class="label">Total Events</div>
208
- <div class="value" id="totalEvents">0</div>
209
- </div>
210
- <div class="stat-card warning">
211
- <div class="label">Pending Blocks</div>
212
- <div class="value" id="pendingBlocks">0</div>
213
- </div>
214
- <div class="stat-card success">
215
- <div class="label">Active Connectors</div>
216
- <div class="value" id="activeConnectors">0</div>
217
- </div>
218
- <div class="stat-card error">
219
- <div class="label">Errors</div>
220
- <div class="value" id="errors">0</div>
221
- </div>
222
- </div>
490
+ .btn-danger {
491
+ background: var(--red);
492
+ color: var(--white);
493
+ }
223
494
 
224
- <div class="section">
225
- <h2>Recent Activity</h2>
226
- <ul class="activity-list" id="activityList">
227
- <li class="empty-state">
228
- <p>No recent activity</p>
229
- </li>
230
- </ul>
231
- </div>
495
+ .btn-small {
496
+ padding: 8px 16px;
497
+ font-size: 0.8rem;
498
+ box-shadow: 2px 2px 0px var(--grey-900);
499
+ }
232
500
 
233
- <script>
234
- // Dashboard state
235
- let ws = null;
236
- let reconnectTimeout = null;
501
+ .btn-small:hover {
502
+ transform: translate(1px, 1px);
503
+ box-shadow: 1px 1px 0px var(--grey-900);
504
+ }
237
505
 
238
- // DOM elements
239
- const connectionDot = document.getElementById('connectionDot');
240
- const connectionText = document.getElementById('connectionText');
241
- const totalEventsEl = document.getElementById('totalEvents');
242
- const pendingBlocksEl = document.getElementById('pendingBlocks');
243
- const activeConnectorsEl = document.getElementById('activeConnectors');
244
- const errorsEl = document.getElementById('errors');
245
- const activityList = document.getElementById('activityList');
506
+ /* Tables */
507
+ .data-table {
508
+ width: 100%;
509
+ border-collapse: collapse;
510
+ }
246
511
 
247
- // Update connection status
248
- function setConnectionStatus(connected) {
249
- connectionDot.className = 'status-dot ' + (connected ? 'connected' : 'disconnected');
250
- connectionText.textContent = connected ? 'Connected' : 'Disconnected';
512
+ .data-table th, .data-table td {
513
+ padding: 12px 16px;
514
+ text-align: left;
515
+ border-bottom: 1px solid var(--grey-200);
251
516
  }
252
517
 
253
- // Update stats display
254
- function updateStats(stats) {
255
- totalEventsEl.textContent = stats.totalEvents || 0;
256
- pendingBlocksEl.textContent = stats.pendingBlocks || 0;
257
- activeConnectorsEl.textContent = stats.connectorCount || 0;
258
- errorsEl.textContent = (stats.eventsByLevel && stats.eventsByLevel.error) || 0;
518
+ .data-table th {
519
+ background: var(--grey-100);
520
+ font-weight: 600;
521
+ font-size: 0.85rem;
522
+ text-transform: uppercase;
523
+ color: var(--grey-600);
259
524
  }
260
525
 
261
- // Format timestamp
262
- function formatTime(timestamp) {
263
- const date = new Date(timestamp);
264
- return date.toLocaleTimeString();
526
+ .data-table tr:hover td {
527
+ background: var(--grey-50);
265
528
  }
266
529
 
267
- // Get icon class based on level
268
- function getIconClass(level) {
269
- switch (level) {
270
- case 'error': return 'error';
271
- case 'warning': return 'warning';
272
- case 'debug': return 'debug';
273
- default: return 'info';
274
- }
530
+ .data-table td {
531
+ font-size: 0.9rem;
275
532
  }
276
533
 
277
- // Get icon symbol based on level
278
- function getIconSymbol(level) {
279
- switch (level) {
280
- case 'error': return '!';
281
- case 'warning': return '!';
282
- case 'debug': return '#';
283
- default: return 'i';
284
- }
534
+ /* Session List */
535
+ .session-list-item {
536
+ display: flex;
537
+ align-items: center;
538
+ padding: 16px;
539
+ border-bottom: 1px solid var(--grey-200);
540
+ cursor: pointer;
541
+ transition: background 0.15s;
285
542
  }
286
543
 
287
- // Update activity list
288
- function updateActivity(events) {
289
- if (!events || events.length === 0) {
290
- activityList.innerHTML = '<li class="empty-state"><p>No recent activity</p></li>';
291
- return;
292
- }
544
+ .session-list-item:hover {
545
+ background: var(--grey-50);
546
+ }
293
547
 
294
- activityList.innerHTML = events.slice(0, 20).map(event => `
295
- <li class="activity-item">
296
- <div class="activity-icon ${getIconClass(event.level)}">
297
- ${getIconSymbol(event.level)}
298
- </div>
299
- <div class="activity-content">
300
- <div class="activity-message">${escapeHtml(event.message)}</div>
301
- <div class="activity-meta">
302
- <span>${event.source}</span>
303
- <span>${event.category}</span>
304
- <span>${formatTime(event.timestamp)}</span>
305
- </div>
306
- </div>
307
- </li>
308
- `).join('');
548
+ .session-list-item.active {
549
+ background: var(--teal-light);
309
550
  }
310
551
 
311
- // Escape HTML to prevent XSS
312
- function escapeHtml(text) {
313
- const div = document.createElement('div');
314
- div.textContent = text;
315
- return div.innerHTML;
552
+ .session-status-indicator {
553
+ width: 12px;
554
+ height: 12px;
555
+ border-radius: 50%;
556
+ margin-right: 16px;
316
557
  }
317
558
 
318
- // Fetch stats from API
319
- async function fetchStats() {
320
- try {
321
- const response = await fetch('/api/stats');
322
- if (response.ok) {
323
- const stats = await response.json();
324
- updateStats(stats);
325
- }
326
- } catch (error) {
327
- console.error('Failed to fetch stats:', error);
328
- }
559
+ .session-status-indicator.active { background: var(--green); }
560
+ .session-status-indicator.completed { background: var(--grey-400); }
561
+ .session-status-indicator.crashed { background: var(--red); }
562
+
563
+ .session-list-info { flex: 1; }
564
+
565
+ .session-list-title {
566
+ font-weight: 600;
567
+ font-size: 0.95rem;
329
568
  }
330
569
 
331
- // Fetch events from API
332
- async function fetchEvents() {
333
- try {
334
- const response = await fetch('/api/events?limit=20');
335
- if (response.ok) {
336
- const events = await response.json();
337
- updateActivity(events);
338
- }
339
- } catch (error) {
340
- console.error('Failed to fetch events:', error);
341
- }
570
+ .session-list-meta {
571
+ font-size: 0.8rem;
572
+ color: var(--grey-500);
573
+ margin-top: 4px;
342
574
  }
343
575
 
344
- // Connect to WebSocket
345
- function connectWebSocket() {
346
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
347
- const wsUrl = `${protocol}//${window.location.host}`;
576
+ .session-list-stats {
577
+ display: flex;
578
+ gap: 24px;
579
+ text-align: right;
580
+ }
348
581
 
349
- try {
350
- ws = new WebSocket(wsUrl);
351
-
352
- ws.onopen = function() {
353
- setConnectionStatus(true);
354
- if (reconnectTimeout) {
355
- clearTimeout(reconnectTimeout);
356
- reconnectTimeout = null;
357
- }
358
- };
582
+ .session-list-stat {
583
+ display: flex;
584
+ flex-direction: column;
585
+ }
359
586
 
360
- ws.onmessage = function(event) {
361
- try {
362
- const message = JSON.parse(event.data);
363
- if (message.type === 'stats') {
364
- updateStats(message.data);
365
- } else if (message.type === 'event') {
366
- fetchEvents();
367
- fetchStats();
368
- } else if (message.type === 'block-approved' || message.type === 'block-denied') {
369
- fetchStats();
370
- }
371
- } catch (error) {
372
- console.error('Failed to parse WebSocket message:', error);
373
- }
374
- };
587
+ .session-list-stat-value {
588
+ font-family: 'JetBrains Mono', monospace;
589
+ font-weight: 700;
590
+ }
375
591
 
376
- ws.onclose = function() {
377
- setConnectionStatus(false);
378
- ws = null;
379
- // Attempt to reconnect after 5 seconds
380
- reconnectTimeout = setTimeout(connectWebSocket, 5000);
381
- };
592
+ .session-list-stat-label {
593
+ font-size: 0.7rem;
594
+ color: var(--grey-500);
595
+ text-transform: uppercase;
596
+ }
382
597
 
383
- ws.onerror = function() {
384
- setConnectionStatus(false);
385
- };
386
- } catch (error) {
387
- console.error('Failed to connect WebSocket:', error);
388
- setConnectionStatus(false);
389
- reconnectTimeout = setTimeout(connectWebSocket, 5000);
390
- }
598
+ /* Activity List */
599
+ .activity-list { list-style: none; }
600
+
601
+ .activity-item {
602
+ display: flex;
603
+ align-items: flex-start;
604
+ gap: 12px;
605
+ padding: 12px 0;
606
+ border-bottom: 1px solid var(--grey-200);
391
607
  }
392
608
 
393
- // Initialize dashboard
394
- function init() {
395
- fetchStats();
396
- fetchEvents();
397
- connectWebSocket();
609
+ .activity-item:last-child { border-bottom: none; }
398
610
 
399
- // Auto-refresh every 30 seconds
400
- setInterval(() => {
401
- fetchStats();
402
- fetchEvents();
403
- }, 30000);
611
+ .activity-icon {
612
+ width: 36px;
613
+ height: 36px;
614
+ display: flex;
615
+ align-items: center;
616
+ justify-content: center;
617
+ font-size: 1rem;
618
+ border: 2px solid var(--grey-900);
619
+ flex-shrink: 0;
620
+ }
621
+
622
+ .activity-icon.info { background: var(--teal-light); }
623
+ .activity-icon.warning { background: var(--yellow); }
624
+ .activity-icon.error { background: var(--red); color: var(--white); }
625
+ .activity-icon.ai { background: var(--purple); color: var(--white); }
626
+
627
+ .activity-content { flex: 1; }
628
+
629
+ .activity-message {
630
+ font-size: 0.9rem;
631
+ word-break: break-word;
632
+ }
633
+
634
+ .activity-meta {
635
+ display: flex;
636
+ gap: 16px;
637
+ margin-top: 4px;
638
+ font-size: 0.8rem;
639
+ color: var(--grey-500);
640
+ font-family: 'JetBrains Mono', monospace;
641
+ }
642
+
643
+ /* Bro Status Panel */
644
+ .bro-status-panel {
645
+ display: flex;
646
+ align-items: center;
647
+ gap: 24px;
648
+ padding: 20px;
649
+ background: var(--grey-50);
650
+ border-bottom: 2px solid var(--grey-300);
651
+ }
652
+
653
+ .bro-avatar {
654
+ width: 64px;
655
+ height: 64px;
656
+ background: var(--purple);
657
+ border: 3px solid var(--grey-900);
658
+ display: flex;
659
+ align-items: center;
660
+ justify-content: center;
661
+ font-size: 2rem;
662
+ }
663
+
664
+ .bro-info { flex: 1; }
665
+
666
+ .bro-title {
667
+ font-size: 1.1rem;
668
+ font-weight: 700;
669
+ display: flex;
670
+ align-items: center;
671
+ gap: 8px;
672
+ }
673
+
674
+ .bro-subtitle {
675
+ font-size: 0.85rem;
676
+ color: var(--grey-600);
677
+ margin-top: 4px;
678
+ }
679
+
680
+ .bro-controls {
681
+ display: flex;
682
+ gap: 12px;
404
683
  }
405
684
 
406
- // Start when DOM is ready
407
- document.addEventListener('DOMContentLoaded', init);
685
+ /* Empty State */
686
+ .empty-state {
687
+ text-align: center;
688
+ padding: 40px 20px;
689
+ color: var(--grey-500);
690
+ }
691
+
692
+ /* Toast */
693
+ .toast {
694
+ position: fixed;
695
+ bottom: 24px;
696
+ right: 24px;
697
+ padding: 16px 24px;
698
+ background: var(--grey-900);
699
+ color: var(--white);
700
+ font-family: 'JetBrains Mono', monospace;
701
+ border: var(--border);
702
+ box-shadow: var(--shadow);
703
+ z-index: 1000;
704
+ display: none;
705
+ }
706
+
707
+ .toast.success { background: var(--teal-dark); }
708
+ .toast.error { background: var(--red); }
709
+ .toast.show { display: block; animation: slideIn 0.3s ease; }
710
+
711
+ @keyframes slideIn {
712
+ from { transform: translateX(100%); opacity: 0; }
713
+ to { transform: translateX(0); opacity: 1; }
714
+ }
715
+
716
+ /* Filter Bar */
717
+ .filter-bar {
718
+ display: flex;
719
+ gap: 12px;
720
+ margin-bottom: 20px;
721
+ flex-wrap: wrap;
722
+ }
723
+
724
+ .filter-btn {
725
+ padding: 8px 16px;
726
+ font-family: 'JetBrains Mono', monospace;
727
+ font-size: 0.8rem;
728
+ border: 2px solid var(--grey-300);
729
+ background: var(--white);
730
+ cursor: pointer;
731
+ transition: all 0.15s;
732
+ }
733
+
734
+ .filter-btn:hover {
735
+ border-color: var(--grey-900);
736
+ }
737
+
738
+ .filter-btn.active {
739
+ background: var(--grey-900);
740
+ color: var(--white);
741
+ border-color: var(--grey-900);
742
+ }
743
+
744
+ /* Responsive */
745
+ @media (max-width: 768px) {
746
+ .nav-tabs { flex-wrap: nowrap; }
747
+ .nav-tab { flex: none; padding: 12px 16px; font-size: 0.8rem; }
748
+ .main { padding: 16px; }
749
+ .session-banner { flex-direction: column; }
750
+ .session-info { gap: 16px; }
751
+ .header { flex-direction: column; gap: 12px; }
752
+ .bro-status-panel { flex-direction: column; text-align: center; }
753
+ }
754
+ </style>
755
+ </head>
756
+ <body>
757
+ <!-- Header -->
758
+ <header class="header">
759
+ <div class="logo">
760
+ <span class="logo-slash">/</span>BashBros Dashboard
761
+ </div>
762
+ <div class="header-right">
763
+ <div class="connection-status">
764
+ <span class="status-dot" id="connectionDot"></span>
765
+ <span id="connectionText">Connecting...</span>
766
+ </div>
767
+ </div>
768
+ </header>
769
+
770
+ <!-- Navigation -->
771
+ <nav class="nav-tabs">
772
+ <button class="nav-tab active" data-tab="live">Live</button>
773
+ <button class="nav-tab" data-tab="sessions">Sessions</button>
774
+ <button class="nav-tab" data-tab="security">Security <span class="nav-badge" id="securityBadge" style="display:none">0</span></button>
775
+ <button class="nav-tab" data-tab="bro">Bash Bro</button>
776
+ <button class="nav-tab" data-tab="settings">Settings</button>
777
+ </nav>
778
+
779
+ <!-- Main Content -->
780
+ <main class="main">
781
+ <!-- Live Tab -->
782
+ <div id="live" class="tab-content active">
783
+ <!-- Session Banner -->
784
+ <div class="session-banner" id="sessionBanner">
785
+ <div class="session-info">
786
+ <div class="session-stat">
787
+ <span class="session-stat-label">Session</span>
788
+ <span class="session-stat-value" id="liveSessionId">-</span>
789
+ </div>
790
+ <div class="session-stat">
791
+ <span class="session-stat-label">Duration</span>
792
+ <span class="session-stat-value" id="liveSessionDuration">-</span>
793
+ </div>
794
+ <div class="session-stat">
795
+ <span class="session-stat-label">Commands</span>
796
+ <span class="session-stat-value" id="liveSessionCommands">0</span>
797
+ </div>
798
+ <div class="session-stat">
799
+ <span class="session-stat-label">Avg Risk</span>
800
+ <span class="session-stat-value" id="liveSessionRisk">-</span>
801
+ </div>
802
+ </div>
803
+ <div id="sessionStatus">
804
+ <span class="status-dot" id="sessionStatusDot"></span>
805
+ <span id="sessionStatusText">No active session</span>
806
+ </div>
807
+ </div>
808
+
809
+ <div class="grid-2">
810
+ <!-- Live Command Feed -->
811
+ <div class="card">
812
+ <div class="card-header">
813
+ Live Command Feed
814
+ <span style="font-size: 0.8rem; color: var(--grey-500);">Auto-updating</span>
815
+ </div>
816
+ <div class="card-body no-padding">
817
+ <div class="live-feed" id="liveCommandFeed">
818
+ <div class="empty-state">Waiting for commands...</div>
819
+ </div>
820
+ </div>
821
+ </div>
822
+
823
+ <!-- Recent Violations -->
824
+ <div class="card">
825
+ <div class="card-header">Recent Violations</div>
826
+ <div class="card-body no-padding">
827
+ <div class="live-feed" id="recentViolations">
828
+ <div class="empty-state">No violations</div>
829
+ </div>
830
+ </div>
831
+ </div>
832
+ </div>
833
+
834
+ <!-- Quick Stats -->
835
+ <div class="stats-grid">
836
+ <div class="stat-card">
837
+ <div class="stat-label">Today's Commands</div>
838
+ <div class="stat-value" id="todayCommands">0</div>
839
+ </div>
840
+ <div class="stat-card">
841
+ <div class="stat-label">Today's Violations</div>
842
+ <div class="stat-value error" id="todayViolations">0</div>
843
+ </div>
844
+ <div class="stat-card">
845
+ <div class="stat-label">Avg Risk (24h)</div>
846
+ <div class="stat-value" id="avgRisk24h">-</div>
847
+ </div>
848
+ <div class="stat-card">
849
+ <div class="stat-label">Ollama Status</div>
850
+ <div class="stat-value" id="ollamaStatusStat">-</div>
851
+ </div>
852
+ </div>
853
+ </div>
854
+
855
+ <!-- Sessions Tab -->
856
+ <div id="sessions" class="tab-content">
857
+ <div class="filter-bar">
858
+ <button class="filter-btn active" data-filter="all" onclick="filterSessions('all')">All</button>
859
+ <button class="filter-btn" data-filter="today" onclick="filterSessions('today')">Today</button>
860
+ <button class="filter-btn" data-filter="week" onclick="filterSessions('week')">This Week</button>
861
+ <button class="filter-btn" data-filter="month" onclick="filterSessions('month')">This Month</button>
862
+ </div>
863
+
864
+ <div class="grid-2">
865
+ <!-- Session List -->
866
+ <div class="card">
867
+ <div class="card-header">Sessions</div>
868
+ <div class="card-body no-padding">
869
+ <div id="sessionList">
870
+ <div class="empty-state">No sessions recorded</div>
871
+ </div>
872
+ </div>
873
+ </div>
874
+
875
+ <!-- Session Detail -->
876
+ <div class="card">
877
+ <div class="card-header">Session Detail</div>
878
+ <div class="card-body" id="sessionDetail">
879
+ <div class="empty-state">Select a session to view details</div>
880
+ </div>
881
+ </div>
882
+ </div>
883
+ </div>
884
+
885
+ <!-- Security Tab -->
886
+ <div id="security" class="tab-content">
887
+ <!-- Pending Egress Blocks -->
888
+ <div class="card">
889
+ <div class="card-header">
890
+ Pending Egress Blocks
891
+ <button class="btn btn-small" onclick="fetchBlocked()">Refresh</button>
892
+ </div>
893
+ <div class="card-body no-padding">
894
+ <table class="data-table">
895
+ <thead>
896
+ <tr>
897
+ <th>Pattern</th>
898
+ <th>Matched Text</th>
899
+ <th>Connector</th>
900
+ <th>Time</th>
901
+ <th>Actions</th>
902
+ </tr>
903
+ </thead>
904
+ <tbody id="egressBlocksTable">
905
+ <tr><td colspan="5" class="empty-state">No pending blocks</td></tr>
906
+ </tbody>
907
+ </table>
908
+ </div>
909
+ </div>
910
+
911
+ <!-- Exposure Scans -->
912
+ <div class="card">
913
+ <div class="card-header">
914
+ Network Exposure Scans
915
+ <button class="btn btn-small" onclick="fetchExposures()">Refresh</button>
916
+ </div>
917
+ <div class="card-body no-padding">
918
+ <table class="data-table">
919
+ <thead>
920
+ <tr>
921
+ <th>Severity</th>
922
+ <th>Port</th>
923
+ <th>Bind Address</th>
924
+ <th>Agent</th>
925
+ <th>Has Auth</th>
926
+ <th>Action</th>
927
+ <th>Message</th>
928
+ </tr>
929
+ </thead>
930
+ <tbody id="exposuresTable">
931
+ <tr><td colspan="7" class="empty-state">No exposure scans</td></tr>
932
+ </tbody>
933
+ </table>
934
+ </div>
935
+ </div>
936
+
937
+ <!-- Violation Summary -->
938
+ <div class="card">
939
+ <div class="card-header">Violation Summary (Last 24h)</div>
940
+ <div class="card-body">
941
+ <div id="violationSummary">
942
+ <div class="empty-state">Loading...</div>
943
+ </div>
944
+ </div>
945
+ </div>
946
+ </div>
947
+
948
+ <!-- Bash Bro Tab -->
949
+ <div id="bro" class="tab-content">
950
+ <!-- Bro Status Panel -->
951
+ <div class="card">
952
+ <div class="bro-status-panel">
953
+ <div class="bro-avatar">AI</div>
954
+ <div class="bro-info">
955
+ <div class="bro-title">
956
+ Bash Bro
957
+ <span class="status-dot" id="broStatusDot"></span>
958
+ </div>
959
+ <div class="bro-subtitle" id="broModelInfo">Checking Ollama connection...</div>
960
+ </div>
961
+ <div class="bro-controls">
962
+ <select class="form-select" id="modelSelect" style="width: auto; min-width: 200px;">
963
+ <option value="">Loading models...</option>
964
+ </select>
965
+ <button class="btn btn-primary btn-small" onclick="changeModel()">Apply</button>
966
+ <button class="btn btn-small" onclick="triggerScan()">Scan System</button>
967
+ </div>
968
+ </div>
969
+
970
+ <div class="card-body">
971
+ <div class="stats-grid">
972
+ <div class="stat-card">
973
+ <div class="stat-label">Platform</div>
974
+ <div class="stat-value" id="broPlatform" style="font-size: 1.2rem;">-</div>
975
+ </div>
976
+ <div class="stat-card">
977
+ <div class="stat-label">Shell</div>
978
+ <div class="stat-value" id="broShell" style="font-size: 1.2rem;">-</div>
979
+ </div>
980
+ <div class="stat-card">
981
+ <div class="stat-label">Project Type</div>
982
+ <div class="stat-value" id="broProjectType" style="font-size: 1.2rem;">-</div>
983
+ </div>
984
+ <div class="stat-card">
985
+ <div class="stat-label">AI Requests Today</div>
986
+ <div class="stat-value info" id="broRequestsToday">0</div>
987
+ </div>
988
+ </div>
989
+ </div>
990
+ </div>
991
+
992
+ <!-- Bro Activity Log -->
993
+ <div class="card">
994
+ <div class="card-header">
995
+ AI Activity Log
996
+ <button class="btn btn-small" onclick="fetchBroEvents()">Refresh</button>
997
+ </div>
998
+ <div class="card-body">
999
+ <ul class="activity-list" id="broActivityLog">
1000
+ <li class="empty-state">No AI activity recorded</li>
1001
+ </ul>
1002
+ </div>
1003
+ </div>
1004
+ </div>
1005
+
1006
+ <!-- Settings Tab -->
1007
+ <div id="settings" class="tab-content">
1008
+ <div class="grid-2">
1009
+ <div>
1010
+ <div class="card">
1011
+ <div class="card-header">
1012
+ Security Profile
1013
+ <button class="btn btn-primary btn-small" onclick="saveConfig()">Save Changes</button>
1014
+ </div>
1015
+ <div class="card-body">
1016
+ <div class="form-group">
1017
+ <label class="form-label">Profile</label>
1018
+ <select class="form-select" id="profileSelect">
1019
+ <option value="permissive">Permissive - Allow all, block dangerous</option>
1020
+ <option value="balanced">Balanced - Explicit allowlist</option>
1021
+ <option value="strict">Strict - Minimal access</option>
1022
+ </select>
1023
+ </div>
1024
+ </div>
1025
+ </div>
1026
+
1027
+ <div class="card">
1028
+ <div class="card-header">Allowed Commands</div>
1029
+ <div class="card-body">
1030
+ <p style="margin-bottom: 12px; color: var(--grey-600); font-size: 0.9rem;">
1031
+ Commands that are always allowed. Use * as wildcard.
1032
+ </p>
1033
+ <div class="list-editor" id="allowList"></div>
1034
+ <div class="list-add">
1035
+ <input type="text" class="form-input" id="allowInput" placeholder="Add command pattern (e.g., docker *)">
1036
+ <button class="btn btn-small" onclick="addToList('allow')">Add</button>
1037
+ </div>
1038
+ </div>
1039
+ </div>
1040
+
1041
+ <div class="card">
1042
+ <div class="card-header">Blocked Commands</div>
1043
+ <div class="card-body">
1044
+ <p style="margin-bottom: 12px; color: var(--grey-600); font-size: 0.9rem;">
1045
+ Commands that are always blocked, even in permissive mode.
1046
+ </p>
1047
+ <div class="list-editor" id="blockList"></div>
1048
+ <div class="list-add">
1049
+ <input type="text" class="form-input" id="blockInput" placeholder="Add blocked pattern (e.g., rm -rf /*)">
1050
+ <button class="btn btn-small" onclick="addToList('block')">Add</button>
1051
+ </div>
1052
+ </div>
1053
+ </div>
1054
+ </div>
1055
+
1056
+ <div>
1057
+ <div class="card">
1058
+ <div class="card-header">Security Features</div>
1059
+ <div class="card-body">
1060
+ <div class="toggle-row">
1061
+ <div class="toggle-info">
1062
+ <h4>Secrets Guard</h4>
1063
+ <p>Block access to .env files, API keys, and credentials</p>
1064
+ </div>
1065
+ <label class="toggle">
1066
+ <input type="checkbox" id="secretsEnabled" checked>
1067
+ <span class="toggle-slider"></span>
1068
+ </label>
1069
+ </div>
1070
+ <div class="toggle-row">
1071
+ <div class="toggle-info">
1072
+ <h4>Audit Log</h4>
1073
+ <p>Record all command executions and violations</p>
1074
+ </div>
1075
+ <label class="toggle">
1076
+ <input type="checkbox" id="auditEnabled" checked>
1077
+ <span class="toggle-slider"></span>
1078
+ </label>
1079
+ </div>
1080
+ <div class="toggle-row">
1081
+ <div class="toggle-info">
1082
+ <h4>Rate Limiter</h4>
1083
+ <p>Prevent runaway agents with command rate limits</p>
1084
+ </div>
1085
+ <label class="toggle">
1086
+ <input type="checkbox" id="rateLimitEnabled" checked>
1087
+ <span class="toggle-slider"></span>
1088
+ </label>
1089
+ </div>
1090
+ <div class="toggle-row">
1091
+ <div class="toggle-info">
1092
+ <h4>Risk Scoring</h4>
1093
+ <p>Score commands by danger level (1-10)</p>
1094
+ </div>
1095
+ <label class="toggle">
1096
+ <input type="checkbox" id="riskScoringEnabled" checked>
1097
+ <span class="toggle-slider"></span>
1098
+ </label>
1099
+ </div>
1100
+ <div class="toggle-row">
1101
+ <div class="toggle-info">
1102
+ <h4>Loop Detection</h4>
1103
+ <p>Detect stuck or repetitive agent behavior</p>
1104
+ </div>
1105
+ <label class="toggle">
1106
+ <input type="checkbox" id="loopDetectionEnabled" checked>
1107
+ <span class="toggle-slider"></span>
1108
+ </label>
1109
+ </div>
1110
+ <div class="toggle-row">
1111
+ <div class="toggle-info">
1112
+ <h4>Anomaly Detection</h4>
1113
+ <p>Flag unusual patterns and suspicious commands</p>
1114
+ </div>
1115
+ <label class="toggle">
1116
+ <input type="checkbox" id="anomalyDetectionEnabled" checked>
1117
+ <span class="toggle-slider"></span>
1118
+ </label>
1119
+ </div>
1120
+ <div class="toggle-row">
1121
+ <div class="toggle-info">
1122
+ <h4>Output Scanning</h4>
1123
+ <p>Detect and redact leaked secrets in output</p>
1124
+ </div>
1125
+ <label class="toggle">
1126
+ <input type="checkbox" id="outputScanningEnabled" checked>
1127
+ <span class="toggle-slider"></span>
1128
+ </label>
1129
+ </div>
1130
+ <div class="toggle-row">
1131
+ <div class="toggle-info">
1132
+ <h4>Undo Stack</h4>
1133
+ <p>Enable rollback of file changes</p>
1134
+ </div>
1135
+ <label class="toggle">
1136
+ <input type="checkbox" id="undoEnabled" checked>
1137
+ <span class="toggle-slider"></span>
1138
+ </label>
1139
+ </div>
1140
+ <div class="toggle-row">
1141
+ <div class="toggle-info">
1142
+ <h4>Ward Security</h4>
1143
+ <p>Network exposure and egress monitoring</p>
1144
+ </div>
1145
+ <label class="toggle">
1146
+ <input type="checkbox" id="wardEnabled" checked>
1147
+ <span class="toggle-slider"></span>
1148
+ </label>
1149
+ </div>
1150
+ </div>
1151
+ </div>
1152
+
1153
+ <div class="card">
1154
+ <div class="card-header">Data Retention</div>
1155
+ <div class="card-body">
1156
+ <div class="form-group">
1157
+ <label class="form-label">Retention Period</label>
1158
+ <select class="form-select" id="retentionSelect">
1159
+ <option value="7">7 days</option>
1160
+ <option value="14">14 days</option>
1161
+ <option value="30" selected>30 days</option>
1162
+ <option value="60">60 days</option>
1163
+ <option value="90">90 days</option>
1164
+ </select>
1165
+ </div>
1166
+ </div>
1167
+ </div>
1168
+ </div>
1169
+ </div>
1170
+ </div>
1171
+ </main>
1172
+
1173
+ <!-- Toast -->
1174
+ <div class="toast" id="toast"></div>
1175
+
1176
+ <script>
1177
+ // State
1178
+ let config = null;
1179
+ let ws = null;
1180
+ let activeTab = 'live';
1181
+ let selectedSessionId = null;
1182
+ let livePollingInterval = null;
1183
+ let backgroundPollingInterval = null;
1184
+ let lastCommandId = null;
1185
+
1186
+ // DOM Elements
1187
+ const connectionDot = document.getElementById('connectionDot');
1188
+ const connectionText = document.getElementById('connectionText');
1189
+ const toast = document.getElementById('toast');
1190
+
1191
+ // Tab Navigation
1192
+ document.querySelectorAll('.nav-tab').forEach(tab => {
1193
+ tab.addEventListener('click', () => {
1194
+ document.querySelectorAll('.nav-tab').forEach(t => t.classList.remove('active'));
1195
+ document.querySelectorAll('.tab-content').forEach(c => c.classList.remove('active'));
1196
+ tab.classList.add('active');
1197
+ const tabId = tab.dataset.tab;
1198
+ document.getElementById(tabId).classList.add('active');
1199
+ activeTab = tabId;
1200
+ onTabChange(tabId);
1201
+ });
1202
+ });
1203
+
1204
+ function onTabChange(tabId) {
1205
+ // Adjust polling based on active tab
1206
+ if (tabId === 'live') {
1207
+ startLivePolling();
1208
+ } else {
1209
+ stopLivePolling();
1210
+ }
1211
+
1212
+ // Load tab-specific data
1213
+ if (tabId === 'sessions') fetchSessions();
1214
+ if (tabId === 'security') {
1215
+ fetchBlocked();
1216
+ fetchExposures();
1217
+ }
1218
+ if (tabId === 'bro') {
1219
+ fetchBroStatus();
1220
+ fetchBroModels();
1221
+ fetchBroEvents();
1222
+ }
1223
+ if (tabId === 'settings') loadConfig();
1224
+ }
1225
+
1226
+ // Show Toast
1227
+ function showToast(message, type = 'success') {
1228
+ toast.textContent = message;
1229
+ toast.className = 'toast ' + type + ' show';
1230
+ setTimeout(() => toast.classList.remove('show'), 3000);
1231
+ }
1232
+
1233
+ // Connection Status
1234
+ function setConnectionStatus(connected) {
1235
+ connectionDot.className = 'status-dot ' + (connected ? 'connected' : 'disconnected');
1236
+ connectionText.textContent = connected ? 'Connected' : 'Disconnected';
1237
+ }
1238
+
1239
+ // ─────────────────────────────────────────────────────────────
1240
+ // Live Tab Functions
1241
+ // ─────────────────────────────────────────────────────────────
1242
+
1243
+ function startLivePolling() {
1244
+ fetchLiveData();
1245
+ if (livePollingInterval) clearInterval(livePollingInterval);
1246
+ livePollingInterval = setInterval(fetchLiveData, 1000);
1247
+ }
1248
+
1249
+ function stopLivePolling() {
1250
+ if (livePollingInterval) {
1251
+ clearInterval(livePollingInterval);
1252
+ livePollingInterval = null;
1253
+ }
1254
+ }
1255
+
1256
+ async function fetchLiveData() {
1257
+ try {
1258
+ // Fetch active session
1259
+ const sessionRes = await fetch('/api/sessions/active');
1260
+ const session = await sessionRes.json();
1261
+ updateSessionBanner(session);
1262
+
1263
+ // Fetch live commands
1264
+ const commandsRes = await fetch('/api/commands/live?limit=30');
1265
+ const commands = await commandsRes.json();
1266
+ renderLiveCommands(commands);
1267
+
1268
+ // Fetch stats
1269
+ const statsRes = await fetch('/api/stats');
1270
+ const stats = await statsRes.json();
1271
+ updateLiveStats(stats);
1272
+ } catch (error) {
1273
+ console.error('Failed to fetch live data:', error);
1274
+ }
1275
+ }
1276
+
1277
+ function updateSessionBanner(session) {
1278
+ const banner = document.getElementById('sessionBanner');
1279
+ const statusDot = document.getElementById('sessionStatusDot');
1280
+ const statusText = document.getElementById('sessionStatusText');
1281
+
1282
+ if (session) {
1283
+ banner.classList.remove('inactive');
1284
+ document.getElementById('liveSessionId').textContent = session.id.slice(0, 8) + '...';
1285
+ document.getElementById('liveSessionCommands').textContent = session.commandCount;
1286
+ document.getElementById('liveSessionRisk').textContent = session.avgRiskScore.toFixed(1);
1287
+
1288
+ // Calculate duration
1289
+ const start = new Date(session.startTime);
1290
+ const duration = Math.floor((Date.now() - start.getTime()) / 1000);
1291
+ const mins = Math.floor(duration / 60);
1292
+ const secs = duration % 60;
1293
+ document.getElementById('liveSessionDuration').textContent = `${mins}m ${secs}s`;
1294
+
1295
+ statusDot.className = 'status-dot connected';
1296
+ statusText.textContent = 'Session Active';
1297
+ } else {
1298
+ banner.classList.add('inactive');
1299
+ document.getElementById('liveSessionId').textContent = '-';
1300
+ document.getElementById('liveSessionDuration').textContent = '-';
1301
+ document.getElementById('liveSessionCommands').textContent = '0';
1302
+ document.getElementById('liveSessionRisk').textContent = '-';
1303
+
1304
+ statusDot.className = 'status-dot disconnected';
1305
+ statusText.textContent = 'No active session';
1306
+ }
1307
+ }
1308
+
1309
+ function renderLiveCommands(commands) {
1310
+ const feed = document.getElementById('liveCommandFeed');
1311
+ const violations = document.getElementById('recentViolations');
1312
+
1313
+ if (!commands || commands.length === 0) {
1314
+ feed.innerHTML = '<div class="empty-state">Waiting for commands...</div>';
1315
+ violations.innerHTML = '<div class="empty-state">No violations</div>';
1316
+ return;
1317
+ }
1318
+
1319
+ // Render all commands
1320
+ feed.innerHTML = commands.map(cmd => `
1321
+ <div class="command-item">
1322
+ <div class="command-status ${cmd.allowed ? 'allowed' : 'blocked'}"></div>
1323
+ <div class="command-content">
1324
+ <div class="command-text">${escapeHtml(cmd.command)}</div>
1325
+ <div class="command-meta">
1326
+ <span class="risk-badge ${cmd.riskLevel}">${cmd.riskLevel} (${cmd.riskScore})</span>
1327
+ <span>${formatTime(cmd.timestamp)}</span>
1328
+ ${cmd.durationMs ? `<span>${cmd.durationMs}ms</span>` : ''}
1329
+ </div>
1330
+ </div>
1331
+ </div>
1332
+ `).join('');
1333
+
1334
+ // Render violations only
1335
+ const blockedCommands = commands.filter(c => !c.allowed);
1336
+ if (blockedCommands.length === 0) {
1337
+ violations.innerHTML = '<div class="empty-state">No violations</div>';
1338
+ } else {
1339
+ violations.innerHTML = blockedCommands.map(cmd => `
1340
+ <div class="command-item">
1341
+ <div class="command-status blocked"></div>
1342
+ <div class="command-content">
1343
+ <div class="command-text">${escapeHtml(cmd.command)}</div>
1344
+ <div class="command-meta">
1345
+ <span class="risk-badge ${cmd.riskLevel}">${cmd.riskLevel}</span>
1346
+ <span>${formatTime(cmd.timestamp)}</span>
1347
+ ${cmd.violations.length > 0 ? `<span>${cmd.violations[0]}</span>` : ''}
1348
+ </div>
1349
+ </div>
1350
+ </div>
1351
+ `).join('');
1352
+ }
1353
+ }
1354
+
1355
+ function updateLiveStats(stats) {
1356
+ document.getElementById('todayCommands').textContent = stats.todayCommands || 0;
1357
+ document.getElementById('todayViolations').textContent = stats.todayViolations || 0;
1358
+ document.getElementById('avgRisk24h').textContent = stats.avgRiskScore24h ? stats.avgRiskScore24h.toFixed(1) : '-';
1359
+
1360
+ const ollamaEl = document.getElementById('ollamaStatusStat');
1361
+ if (stats.ollamaStatus === 'connected') {
1362
+ ollamaEl.textContent = 'ON';
1363
+ ollamaEl.className = 'stat-value success';
1364
+ } else if (stats.ollamaStatus === 'disconnected') {
1365
+ ollamaEl.textContent = 'OFF';
1366
+ ollamaEl.className = 'stat-value error';
1367
+ } else {
1368
+ ollamaEl.textContent = '-';
1369
+ ollamaEl.className = 'stat-value';
1370
+ }
1371
+
1372
+ // Update security badge
1373
+ const badge = document.getElementById('securityBadge');
1374
+ if (stats.pendingBlocks > 0) {
1375
+ badge.textContent = stats.pendingBlocks;
1376
+ badge.style.display = 'inline';
1377
+ } else {
1378
+ badge.style.display = 'none';
1379
+ }
1380
+ }
1381
+
1382
+ // ─────────────────────────────────────────────────────────────
1383
+ // Sessions Tab Functions
1384
+ // ─────────────────────────────────────────────────────────────
1385
+
1386
+ let sessionFilter = 'all';
1387
+
1388
+ function filterSessions(filter) {
1389
+ sessionFilter = filter;
1390
+ document.querySelectorAll('.filter-btn').forEach(btn => {
1391
+ btn.classList.toggle('active', btn.dataset.filter === filter);
1392
+ });
1393
+ fetchSessions();
1394
+ }
1395
+
1396
+ async function fetchSessions() {
1397
+ try {
1398
+ let url = '/api/sessions?limit=50';
1399
+
1400
+ if (sessionFilter === 'today') {
1401
+ const today = new Date();
1402
+ today.setHours(0, 0, 0, 0);
1403
+ url += `&since=${today.toISOString()}`;
1404
+ } else if (sessionFilter === 'week') {
1405
+ const week = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
1406
+ url += `&since=${week.toISOString()}`;
1407
+ } else if (sessionFilter === 'month') {
1408
+ const month = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
1409
+ url += `&since=${month.toISOString()}`;
1410
+ }
1411
+
1412
+ const response = await fetch(url);
1413
+ const sessions = await response.json();
1414
+ renderSessionList(sessions);
1415
+ } catch (error) {
1416
+ console.error('Failed to fetch sessions:', error);
1417
+ }
1418
+ }
1419
+
1420
+ function renderSessionList(sessions) {
1421
+ const list = document.getElementById('sessionList');
1422
+
1423
+ if (!sessions || sessions.length === 0) {
1424
+ list.innerHTML = '<div class="empty-state">No sessions recorded</div>';
1425
+ return;
1426
+ }
1427
+
1428
+ list.innerHTML = sessions.map(session => `
1429
+ <div class="session-list-item ${selectedSessionId === session.id ? 'active' : ''}"
1430
+ onclick="selectSession('${session.id}')">
1431
+ <div class="session-status-indicator ${session.status}"></div>
1432
+ <div class="session-list-info">
1433
+ <div class="session-list-title">${session.agent} - ${session.id.slice(0, 8)}...</div>
1434
+ <div class="session-list-meta">
1435
+ ${formatDateTime(session.startTime)}
1436
+ ${session.endTime ? ` - ${formatDateTime(session.endTime)}` : ' (active)'}
1437
+ </div>
1438
+ </div>
1439
+ <div class="session-list-stats">
1440
+ <div class="session-list-stat">
1441
+ <span class="session-list-stat-value">${session.commandCount}</span>
1442
+ <span class="session-list-stat-label">Commands</span>
1443
+ </div>
1444
+ <div class="session-list-stat">
1445
+ <span class="session-list-stat-value" style="color: var(--red)">${session.blockedCount}</span>
1446
+ <span class="session-list-stat-label">Blocked</span>
1447
+ </div>
1448
+ <div class="session-list-stat">
1449
+ <span class="session-list-stat-value">${session.avgRiskScore.toFixed(1)}</span>
1450
+ <span class="session-list-stat-label">Avg Risk</span>
1451
+ </div>
1452
+ </div>
1453
+ </div>
1454
+ `).join('');
1455
+ }
1456
+
1457
+ async function selectSession(sessionId) {
1458
+ selectedSessionId = sessionId;
1459
+ fetchSessions(); // Re-render to show selection
1460
+
1461
+ try {
1462
+ // Fetch session details
1463
+ const [sessionRes, metricsRes, commandsRes] = await Promise.all([
1464
+ fetch(`/api/sessions/${sessionId}`),
1465
+ fetch(`/api/sessions/${sessionId}/metrics`),
1466
+ fetch(`/api/sessions/${sessionId}/commands?limit=20`)
1467
+ ]);
1468
+
1469
+ const session = await sessionRes.json();
1470
+ const metrics = await metricsRes.json();
1471
+ const commands = await commandsRes.json();
1472
+
1473
+ renderSessionDetail(session, metrics, commands);
1474
+ } catch (error) {
1475
+ console.error('Failed to fetch session detail:', error);
1476
+ }
1477
+ }
1478
+
1479
+ function renderSessionDetail(session, metrics, commands) {
1480
+ const detail = document.getElementById('sessionDetail');
1481
+
1482
+ detail.innerHTML = `
1483
+ <div style="margin-bottom: 20px;">
1484
+ <h3 style="margin-bottom: 8px;">${session.agent} Session</h3>
1485
+ <p style="color: var(--grey-600); font-size: 0.9rem;">
1486
+ ${session.workingDir}<br>
1487
+ Started: ${formatDateTime(session.startTime)}<br>
1488
+ ${session.endTime ? `Ended: ${formatDateTime(session.endTime)}` : 'Currently active'}
1489
+ </p>
1490
+ </div>
1491
+
1492
+ <div class="stats-grid" style="margin-bottom: 20px;">
1493
+ <div class="stat-card">
1494
+ <div class="stat-label">Total Commands</div>
1495
+ <div class="stat-value" style="font-size: 1.5rem;">${metrics.totalCommands}</div>
1496
+ </div>
1497
+ <div class="stat-card">
1498
+ <div class="stat-label">Allowed</div>
1499
+ <div class="stat-value success" style="font-size: 1.5rem;">${metrics.allowedCommands}</div>
1500
+ </div>
1501
+ <div class="stat-card">
1502
+ <div class="stat-label">Blocked</div>
1503
+ <div class="stat-value error" style="font-size: 1.5rem;">${metrics.blockedCommands}</div>
1504
+ </div>
1505
+ <div class="stat-card">
1506
+ <div class="stat-label">Avg Risk</div>
1507
+ <div class="stat-value" style="font-size: 1.5rem;">${metrics.avgRiskScore.toFixed(1)}</div>
1508
+ </div>
1509
+ </div>
1510
+
1511
+ <h4 style="margin-bottom: 12px;">Risk Distribution</h4>
1512
+ <div style="display: flex; gap: 12px; margin-bottom: 20px; flex-wrap: wrap;">
1513
+ ${Object.entries(metrics.riskDistribution).map(([level, count]) => `
1514
+ <div class="risk-badge ${level}" style="padding: 8px 16px; font-size: 0.9rem;">
1515
+ ${level}: ${count}
1516
+ </div>
1517
+ `).join('')}
1518
+ </div>
1519
+
1520
+ <h4 style="margin-bottom: 12px;">Recent Commands</h4>
1521
+ <div style="max-height: 300px; overflow-y: auto; border: 1px solid var(--grey-200);">
1522
+ ${commands.map(cmd => `
1523
+ <div class="command-item" style="padding: 8px 12px;">
1524
+ <div class="command-status ${cmd.allowed ? 'allowed' : 'blocked'}"></div>
1525
+ <div class="command-content">
1526
+ <div class="command-text" style="font-size: 0.85rem;">${escapeHtml(cmd.command)}</div>
1527
+ <div class="command-meta">
1528
+ <span class="risk-badge ${cmd.riskLevel}">${cmd.riskScore}</span>
1529
+ </div>
1530
+ </div>
1531
+ </div>
1532
+ `).join('')}
1533
+ </div>
1534
+ `;
1535
+ }
1536
+
1537
+ // ─────────────────────────────────────────────────────────────
1538
+ // Security Tab Functions
1539
+ // ─────────────────────────────────────────────────────────────
1540
+
1541
+ async function fetchBlocked() {
1542
+ try {
1543
+ const response = await fetch('/api/blocked');
1544
+ const blocks = await response.json();
1545
+ renderEgressBlocks(blocks);
1546
+ } catch (error) {
1547
+ console.error('Failed to fetch blocks:', error);
1548
+ }
1549
+ }
1550
+
1551
+ function renderEgressBlocks(blocks) {
1552
+ const tbody = document.getElementById('egressBlocksTable');
1553
+
1554
+ if (!blocks || blocks.length === 0) {
1555
+ tbody.innerHTML = '<tr><td colspan="5" class="empty-state">No pending blocks</td></tr>';
1556
+ return;
1557
+ }
1558
+
1559
+ tbody.innerHTML = blocks.map(block => `
1560
+ <tr>
1561
+ <td><code>${escapeHtml(block.pattern?.name || 'Unknown')}</code></td>
1562
+ <td style="max-width: 200px; overflow: hidden; text-overflow: ellipsis;">
1563
+ <code>${escapeHtml(block.redactedText)}</code>
1564
+ </td>
1565
+ <td>${block.connector || '-'}</td>
1566
+ <td>${formatTime(block.timestamp)}</td>
1567
+ <td>
1568
+ <button class="btn btn-small btn-primary" onclick="approveBlock('${block.id}')" style="margin-right: 8px;">Approve</button>
1569
+ <button class="btn btn-small btn-danger" onclick="denyBlock('${block.id}')">Deny</button>
1570
+ </td>
1571
+ </tr>
1572
+ `).join('');
1573
+ }
1574
+
1575
+ async function approveBlock(id) {
1576
+ try {
1577
+ await fetch(`/api/blocked/${id}/approve`, { method: 'POST' });
1578
+ showToast('Block approved');
1579
+ fetchBlocked();
1580
+ } catch (error) {
1581
+ showToast('Failed to approve', 'error');
1582
+ }
1583
+ }
1584
+
1585
+ async function denyBlock(id) {
1586
+ try {
1587
+ await fetch(`/api/blocked/${id}/deny`, { method: 'POST' });
1588
+ showToast('Block denied');
1589
+ fetchBlocked();
1590
+ } catch (error) {
1591
+ showToast('Failed to deny', 'error');
1592
+ }
1593
+ }
1594
+
1595
+ async function fetchExposures() {
1596
+ try {
1597
+ const response = await fetch('/api/exposures?limit=50');
1598
+ const exposures = await response.json();
1599
+ renderExposures(exposures);
1600
+ } catch (error) {
1601
+ console.error('Failed to fetch exposures:', error);
1602
+ }
1603
+ }
1604
+
1605
+ function renderExposures(exposures) {
1606
+ const tbody = document.getElementById('exposuresTable');
1607
+
1608
+ if (!exposures || exposures.length === 0) {
1609
+ tbody.innerHTML = '<tr><td colspan="7" class="empty-state">No exposure scans</td></tr>';
1610
+ return;
1611
+ }
1612
+
1613
+ tbody.innerHTML = exposures.map(exp => `
1614
+ <tr>
1615
+ <td><span class="risk-badge ${exp.severity === 'critical' ? 'critical' : exp.severity === 'high' ? 'dangerous' : exp.severity === 'medium' ? 'caution' : 'safe'}">${exp.severity}</span></td>
1616
+ <td>${exp.port}</td>
1617
+ <td><code>${exp.bindAddress}</code></td>
1618
+ <td>${exp.agent}</td>
1619
+ <td>${exp.hasAuth === true ? 'Yes' : exp.hasAuth === false ? 'No' : 'Unknown'}</td>
1620
+ <td>${exp.action}</td>
1621
+ <td style="max-width: 300px;">${escapeHtml(exp.message)}</td>
1622
+ </tr>
1623
+ `).join('');
1624
+ }
1625
+
1626
+ // ─────────────────────────────────────────────────────────────
1627
+ // Bash Bro Tab Functions
1628
+ // ─────────────────────────────────────────────────────────────
1629
+
1630
+ async function fetchBroStatus() {
1631
+ try {
1632
+ const response = await fetch('/api/bro/status');
1633
+ const status = await response.json();
1634
+
1635
+ const statusDot = document.getElementById('broStatusDot');
1636
+ const modelInfo = document.getElementById('broModelInfo');
1637
+
1638
+ if (status && status.ollamaAvailable) {
1639
+ statusDot.className = 'status-dot connected';
1640
+ modelInfo.textContent = `Connected - Model: ${status.ollamaModel}`;
1641
+
1642
+ document.getElementById('broPlatform').textContent = status.platform || '-';
1643
+ document.getElementById('broShell').textContent = status.shell || '-';
1644
+ document.getElementById('broProjectType').textContent = status.projectType || 'Unknown';
1645
+ } else {
1646
+ statusDot.className = 'status-dot disconnected';
1647
+ modelInfo.textContent = 'Ollama not connected';
1648
+
1649
+ document.getElementById('broPlatform').textContent = '-';
1650
+ document.getElementById('broShell').textContent = '-';
1651
+ document.getElementById('broProjectType').textContent = '-';
1652
+ }
1653
+ } catch (error) {
1654
+ console.error('Failed to fetch Bro status:', error);
1655
+ }
1656
+ }
1657
+
1658
+ async function fetchBroModels() {
1659
+ try {
1660
+ const response = await fetch('/api/bro/models');
1661
+ const data = await response.json();
1662
+
1663
+ const select = document.getElementById('modelSelect');
1664
+
1665
+ if (data.available && data.models.length > 0) {
1666
+ select.innerHTML = data.models.map(model =>
1667
+ `<option value="${model}">${model}</option>`
1668
+ ).join('');
1669
+ } else {
1670
+ select.innerHTML = '<option value="">Ollama not available</option>';
1671
+ }
1672
+ } catch (error) {
1673
+ console.error('Failed to fetch models:', error);
1674
+ }
1675
+ }
1676
+
1677
+ async function fetchBroEvents() {
1678
+ try {
1679
+ const response = await fetch('/api/bro/events?limit=50');
1680
+ const events = await response.json();
1681
+ renderBroEvents(events);
1682
+
1683
+ // Count today's events
1684
+ const today = new Date();
1685
+ today.setHours(0, 0, 0, 0);
1686
+ const todayEvents = events.filter(e => new Date(e.timestamp) >= today);
1687
+ document.getElementById('broRequestsToday').textContent = todayEvents.length;
1688
+ } catch (error) {
1689
+ console.error('Failed to fetch Bro events:', error);
1690
+ }
1691
+ }
1692
+
1693
+ function renderBroEvents(events) {
1694
+ const list = document.getElementById('broActivityLog');
1695
+
1696
+ if (!events || events.length === 0) {
1697
+ list.innerHTML = '<li class="empty-state">No AI activity recorded</li>';
1698
+ return;
1699
+ }
1700
+
1701
+ list.innerHTML = events.map(event => `
1702
+ <li class="activity-item">
1703
+ <div class="activity-icon ai">${getEventIcon(event.eventType)}</div>
1704
+ <div class="activity-content">
1705
+ <div class="activity-message">
1706
+ <strong>${formatEventType(event.eventType)}</strong><br>
1707
+ Input: ${escapeHtml(event.inputContext.slice(0, 100))}${event.inputContext.length > 100 ? '...' : ''}<br>
1708
+ ${event.success ? `Output: ${escapeHtml(event.outputSummary.slice(0, 100))}${event.outputSummary.length > 100 ? '...' : ''}` : '<span style="color:var(--red)">Failed</span>'}
1709
+ </div>
1710
+ <div class="activity-meta">
1711
+ <span>${event.modelUsed}</span>
1712
+ <span>${event.latencyMs}ms</span>
1713
+ <span>${formatTime(event.timestamp)}</span>
1714
+ </div>
1715
+ </div>
1716
+ </li>
1717
+ `).join('');
1718
+ }
1719
+
1720
+ function getEventIcon(type) {
1721
+ const icons = {
1722
+ 'suggestion': '?',
1723
+ 'explanation': 'i',
1724
+ 'fix': 'F',
1725
+ 'script': 'S',
1726
+ 'safety': '!'
1727
+ };
1728
+ return icons[type] || 'AI';
1729
+ }
1730
+
1731
+ function formatEventType(type) {
1732
+ const names = {
1733
+ 'suggestion': 'Command Suggestion',
1734
+ 'explanation': 'Command Explanation',
1735
+ 'fix': 'Error Fix',
1736
+ 'script': 'Script Generation',
1737
+ 'safety': 'Safety Analysis'
1738
+ };
1739
+ return names[type] || type;
1740
+ }
1741
+
1742
+ async function changeModel() {
1743
+ const select = document.getElementById('modelSelect');
1744
+ const model = select.value;
1745
+
1746
+ if (!model) {
1747
+ showToast('Select a model first', 'error');
1748
+ return;
1749
+ }
1750
+
1751
+ try {
1752
+ const response = await fetch('/api/bro/model', {
1753
+ method: 'POST',
1754
+ headers: { 'Content-Type': 'application/json' },
1755
+ body: JSON.stringify({ model })
1756
+ });
1757
+
1758
+ if (response.ok) {
1759
+ showToast(`Model changed to ${model}`);
1760
+ } else {
1761
+ showToast('Failed to change model', 'error');
1762
+ }
1763
+ } catch (error) {
1764
+ showToast('Failed to change model', 'error');
1765
+ }
1766
+ }
1767
+
1768
+ async function triggerScan() {
1769
+ try {
1770
+ const response = await fetch('/api/bro/scan', { method: 'POST' });
1771
+
1772
+ if (response.ok) {
1773
+ showToast('System scan requested');
1774
+ } else {
1775
+ showToast('Failed to trigger scan', 'error');
1776
+ }
1777
+ } catch (error) {
1778
+ showToast('Failed to trigger scan', 'error');
1779
+ }
1780
+ }
1781
+
1782
+ // ─────────────────────────────────────────────────────────────
1783
+ // Settings Tab Functions
1784
+ // ─────────────────────────────────────────────────────────────
1785
+
1786
+ function renderList(type) {
1787
+ const listEl = document.getElementById(type + 'List');
1788
+ const items = type === 'allow' ? config.commands?.allow : config.commands?.block;
1789
+
1790
+ if (!items || items.length === 0) {
1791
+ listEl.innerHTML = '<div class="empty-state">No items</div>';
1792
+ return;
1793
+ }
1794
+
1795
+ listEl.innerHTML = items.map((item, idx) => `
1796
+ <div class="list-item">
1797
+ <span class="list-item-text">${escapeHtml(item)}</span>
1798
+ <button class="list-item-remove" onclick="removeFromList('${type}', ${idx})">x</button>
1799
+ </div>
1800
+ `).join('');
1801
+ }
1802
+
1803
+ function addToList(type) {
1804
+ const input = document.getElementById(type + 'Input');
1805
+ const value = input.value.trim();
1806
+ if (!value) return;
1807
+
1808
+ if (!config.commands) config.commands = { allow: [], block: [] };
1809
+
1810
+ if (type === 'allow') {
1811
+ if (!config.commands.allow) config.commands.allow = [];
1812
+ config.commands.allow.push(value);
1813
+ } else {
1814
+ if (!config.commands.block) config.commands.block = [];
1815
+ config.commands.block.push(value);
1816
+ }
1817
+
1818
+ input.value = '';
1819
+ renderList(type);
1820
+ }
1821
+
1822
+ function removeFromList(type, idx) {
1823
+ if (type === 'allow') {
1824
+ config.commands.allow.splice(idx, 1);
1825
+ } else {
1826
+ config.commands.block.splice(idx, 1);
1827
+ }
1828
+ renderList(type);
1829
+ }
1830
+
1831
+ async function loadConfig() {
1832
+ try {
1833
+ const response = await fetch('/api/config');
1834
+ if (response.ok) {
1835
+ config = await response.json();
1836
+ updateSettingsUI();
1837
+ } else {
1838
+ showToast('Failed to load config', 'error');
1839
+ }
1840
+ } catch (error) {
1841
+ console.error('Failed to load config:', error);
1842
+ showToast('Failed to load config', 'error');
1843
+ }
1844
+ }
1845
+
1846
+ async function saveConfig() {
1847
+ config.profile = document.getElementById('profileSelect').value;
1848
+ config.secrets = config.secrets || {};
1849
+ config.secrets.enabled = document.getElementById('secretsEnabled').checked;
1850
+ config.audit = config.audit || {};
1851
+ config.audit.enabled = document.getElementById('auditEnabled').checked;
1852
+ config.rateLimit = config.rateLimit || {};
1853
+ config.rateLimit.enabled = document.getElementById('rateLimitEnabled').checked;
1854
+ config.riskScoring = config.riskScoring || {};
1855
+ config.riskScoring.enabled = document.getElementById('riskScoringEnabled').checked;
1856
+ config.loopDetection = config.loopDetection || {};
1857
+ config.loopDetection.enabled = document.getElementById('loopDetectionEnabled').checked;
1858
+ config.anomalyDetection = config.anomalyDetection || {};
1859
+ config.anomalyDetection.enabled = document.getElementById('anomalyDetectionEnabled').checked;
1860
+ config.outputScanning = config.outputScanning || {};
1861
+ config.outputScanning.enabled = document.getElementById('outputScanningEnabled').checked;
1862
+ config.undo = config.undo || {};
1863
+ config.undo.enabled = document.getElementById('undoEnabled').checked;
1864
+ config.ward = config.ward || {};
1865
+ config.ward.enabled = document.getElementById('wardEnabled').checked;
1866
+
1867
+ try {
1868
+ const response = await fetch('/api/config', {
1869
+ method: 'POST',
1870
+ headers: { 'Content-Type': 'application/json' },
1871
+ body: JSON.stringify(config)
1872
+ });
1873
+
1874
+ if (response.ok) {
1875
+ showToast('Configuration saved!', 'success');
1876
+ } else {
1877
+ showToast('Failed to save config', 'error');
1878
+ }
1879
+ } catch (error) {
1880
+ console.error('Failed to save config:', error);
1881
+ showToast('Failed to save config', 'error');
1882
+ }
1883
+ }
1884
+
1885
+ function updateSettingsUI() {
1886
+ if (!config) return;
1887
+
1888
+ document.getElementById('profileSelect').value = config.profile || 'permissive';
1889
+
1890
+ document.getElementById('secretsEnabled').checked = config.secrets?.enabled !== false;
1891
+ document.getElementById('auditEnabled').checked = config.audit?.enabled !== false;
1892
+ document.getElementById('rateLimitEnabled').checked = config.rateLimit?.enabled !== false;
1893
+ document.getElementById('riskScoringEnabled').checked = config.riskScoring?.enabled !== false;
1894
+ document.getElementById('loopDetectionEnabled').checked = config.loopDetection?.enabled !== false;
1895
+ document.getElementById('anomalyDetectionEnabled').checked = config.anomalyDetection?.enabled !== false;
1896
+ document.getElementById('outputScanningEnabled').checked = config.outputScanning?.enabled !== false;
1897
+ document.getElementById('undoEnabled').checked = config.undo?.enabled !== false;
1898
+ document.getElementById('wardEnabled').checked = config.ward?.enabled !== false;
1899
+
1900
+ renderList('allow');
1901
+ renderList('block');
1902
+ }
1903
+
1904
+ // ─────────────────────────────────────────────────────────────
1905
+ // Utility Functions
1906
+ // ─────────────────────────────────────────────────────────────
1907
+
1908
+ function formatTime(ts) {
1909
+ if (!ts) return '-';
1910
+ return new Date(ts).toLocaleTimeString();
1911
+ }
1912
+
1913
+ function formatDateTime(ts) {
1914
+ if (!ts) return '-';
1915
+ return new Date(ts).toLocaleString();
1916
+ }
1917
+
1918
+ function escapeHtml(text) {
1919
+ if (!text) return '';
1920
+ const div = document.createElement('div');
1921
+ div.textContent = text;
1922
+ return div.innerHTML;
1923
+ }
1924
+
1925
+ // ─────────────────────────────────────────────────────────────
1926
+ // WebSocket
1927
+ // ─────────────────────────────────────────────────────────────
1928
+
1929
+ function connectWebSocket() {
1930
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
1931
+ try {
1932
+ ws = new WebSocket(`${protocol}//${window.location.host}`);
1933
+ ws.onopen = () => setConnectionStatus(true);
1934
+ ws.onclose = () => {
1935
+ setConnectionStatus(false);
1936
+ setTimeout(connectWebSocket, 5000);
1937
+ };
1938
+ ws.onmessage = (e) => {
1939
+ try {
1940
+ const msg = JSON.parse(e.data);
1941
+ if (msg.type === 'command' && activeTab === 'live') {
1942
+ fetchLiveData();
1943
+ }
1944
+ } catch (err) {}
1945
+ };
1946
+ } catch (err) {
1947
+ setConnectionStatus(false);
1948
+ setTimeout(connectWebSocket, 5000);
1949
+ }
1950
+ }
1951
+
1952
+ // ─────────────────────────────────────────────────────────────
1953
+ // Background Polling
1954
+ // ─────────────────────────────────────────────────────────────
1955
+
1956
+ function startBackgroundPolling() {
1957
+ backgroundPollingInterval = setInterval(() => {
1958
+ // Refresh data periodically even on non-active tabs
1959
+ if (activeTab !== 'live') {
1960
+ fetchStats();
1961
+ }
1962
+ }, 30000);
1963
+ }
1964
+
1965
+ async function fetchStats() {
1966
+ try {
1967
+ const response = await fetch('/api/stats');
1968
+ const stats = await response.json();
1969
+
1970
+ // Update security badge
1971
+ const badge = document.getElementById('securityBadge');
1972
+ if (stats.pendingBlocks > 0) {
1973
+ badge.textContent = stats.pendingBlocks;
1974
+ badge.style.display = 'inline';
1975
+ } else {
1976
+ badge.style.display = 'none';
1977
+ }
1978
+ } catch (error) {
1979
+ console.error('Failed to fetch stats:', error);
1980
+ }
1981
+ }
1982
+
1983
+ // ─────────────────────────────────────────────────────────────
1984
+ // Visibility API - pause polling when tab not visible
1985
+ // ─────────────────────────────────────────────────────────────
1986
+
1987
+ document.addEventListener('visibilitychange', () => {
1988
+ if (document.hidden) {
1989
+ stopLivePolling();
1990
+ } else if (activeTab === 'live') {
1991
+ startLivePolling();
1992
+ }
1993
+ });
1994
+
1995
+ // ─────────────────────────────────────────────────────────────
1996
+ // Init
1997
+ // ─────────────────────────────────────────────────────────────
1998
+
1999
+ document.addEventListener('DOMContentLoaded', () => {
2000
+ loadConfig();
2001
+ connectWebSocket();
2002
+ startLivePolling();
2003
+ startBackgroundPolling();
2004
+ });
408
2005
  </script>
409
2006
  </body>
410
2007
  </html>