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.
- package/dist/{chunk-VVSCAH2B.js → chunk-2RPTM6EQ.js} +211 -8
- package/dist/chunk-2RPTM6EQ.js.map +1 -0
- package/dist/chunk-EYO44OMN.js +181 -0
- package/dist/chunk-EYO44OMN.js.map +1 -0
- package/dist/chunk-FRMAIRQ2.js +89 -0
- package/dist/chunk-FRMAIRQ2.js.map +1 -0
- package/dist/chunk-JYWQT2B4.js +866 -0
- package/dist/chunk-JYWQT2B4.js.map +1 -0
- package/dist/{chunk-GD5VNHIN.js → chunk-QWZGB4V3.js} +4 -85
- package/dist/chunk-QWZGB4V3.js.map +1 -0
- package/dist/cli.js +520 -23
- package/dist/cli.js.map +1 -1
- package/dist/{db-EHQDB5OL.js → db-SWJUUSFX.js} +2 -2
- package/dist/engine-EGPAS2EX.js +10 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +5 -3
- package/dist/index.js.map +1 -1
- package/dist/session-Y4MICATZ.js +15 -0
- package/dist/session-Y4MICATZ.js.map +1 -0
- package/dist/static/index.html +1873 -276
- package/dist/writer-4ZEAKUFD.js +12 -0
- package/dist/writer-4ZEAKUFD.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-CSRPOGHY.js +0 -354
- package/dist/chunk-CSRPOGHY.js.map +0 -1
- package/dist/chunk-GD5VNHIN.js.map +0 -1
- package/dist/chunk-VVSCAH2B.js.map +0 -1
- package/dist/engine-PKLXW6OF.js +0 -9
- /package/dist/{db-EHQDB5OL.js.map → db-SWJUUSFX.js.map} +0 -0
- /package/dist/{engine-PKLXW6OF.js.map → engine-EGPAS2EX.js.map} +0 -0
package/dist/static/index.html
CHANGED
|
@@ -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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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,
|
|
16
|
-
background:
|
|
17
|
-
color:
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
55
|
+
position: sticky;
|
|
56
|
+
top: 0;
|
|
57
|
+
z-index: 100;
|
|
29
58
|
}
|
|
30
59
|
|
|
31
|
-
.
|
|
32
|
-
font-
|
|
33
|
-
font-weight:
|
|
34
|
-
|
|
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:
|
|
42
|
-
color:
|
|
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:
|
|
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
|
-
|
|
53
|
-
|
|
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
|
-
|
|
57
|
-
|
|
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(
|
|
198
|
+
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
|
63
199
|
gap: 20px;
|
|
64
|
-
margin-bottom:
|
|
200
|
+
margin-bottom: 24px;
|
|
65
201
|
}
|
|
66
202
|
|
|
67
203
|
.stat-card {
|
|
68
|
-
background:
|
|
69
|
-
border
|
|
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-
|
|
75
|
-
font-size:
|
|
76
|
-
color:
|
|
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-
|
|
81
|
-
font-
|
|
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-
|
|
87
|
-
|
|
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
|
-
.
|
|
91
|
-
|
|
242
|
+
.session-banner.inactive {
|
|
243
|
+
background: var(--grey-200);
|
|
92
244
|
}
|
|
93
245
|
|
|
94
|
-
.
|
|
95
|
-
|
|
246
|
+
.session-info {
|
|
247
|
+
display: flex;
|
|
248
|
+
gap: 32px;
|
|
249
|
+
flex-wrap: wrap;
|
|
96
250
|
}
|
|
97
251
|
|
|
98
|
-
.
|
|
99
|
-
|
|
100
|
-
|
|
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
|
-
.
|
|
107
|
-
font-size:
|
|
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
|
-
.
|
|
114
|
-
|
|
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
|
-
.
|
|
276
|
+
.command-item {
|
|
118
277
|
display: flex;
|
|
119
278
|
align-items: flex-start;
|
|
120
279
|
gap: 12px;
|
|
121
|
-
padding: 12px
|
|
122
|
-
border-bottom: 1px solid
|
|
280
|
+
padding: 12px 16px;
|
|
281
|
+
border-bottom: 1px solid var(--grey-200);
|
|
282
|
+
transition: background 0.15s;
|
|
123
283
|
}
|
|
124
284
|
|
|
125
|
-
.
|
|
126
|
-
|
|
285
|
+
.command-item:hover {
|
|
286
|
+
background: var(--grey-50);
|
|
127
287
|
}
|
|
128
288
|
|
|
129
|
-
.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
136
|
-
|
|
137
|
-
flex-shrink: 0;
|
|
362
|
+
padding: 12px 0;
|
|
363
|
+
border-bottom: 1px solid var(--grey-200);
|
|
138
364
|
}
|
|
139
365
|
|
|
140
|
-
.
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
.
|
|
146
|
-
|
|
147
|
-
color:
|
|
374
|
+
.toggle-info p {
|
|
375
|
+
font-size: 0.85rem;
|
|
376
|
+
color: var(--grey-600);
|
|
148
377
|
}
|
|
149
378
|
|
|
150
|
-
.
|
|
151
|
-
|
|
152
|
-
|
|
379
|
+
.toggle {
|
|
380
|
+
position: relative;
|
|
381
|
+
width: 52px;
|
|
382
|
+
height: 28px;
|
|
383
|
+
cursor: pointer;
|
|
153
384
|
}
|
|
154
385
|
|
|
155
|
-
.
|
|
156
|
-
|
|
157
|
-
|
|
386
|
+
.toggle input {
|
|
387
|
+
opacity: 0;
|
|
388
|
+
width: 0;
|
|
389
|
+
height: 0;
|
|
158
390
|
}
|
|
159
391
|
|
|
160
|
-
.
|
|
161
|
-
|
|
162
|
-
|
|
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
|
-
.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
.
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
.
|
|
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:
|
|
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
|
-
.
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
.
|
|
192
|
-
|
|
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
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
501
|
+
.btn-small:hover {
|
|
502
|
+
transform: translate(1px, 1px);
|
|
503
|
+
box-shadow: 1px 1px 0px var(--grey-900);
|
|
504
|
+
}
|
|
237
505
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
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
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
262
|
-
|
|
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
|
-
|
|
268
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
-
|
|
295
|
-
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
552
|
+
.session-status-indicator {
|
|
553
|
+
width: 12px;
|
|
554
|
+
height: 12px;
|
|
555
|
+
border-radius: 50%;
|
|
556
|
+
margin-right: 16px;
|
|
316
557
|
}
|
|
317
558
|
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
576
|
+
.session-list-stats {
|
|
577
|
+
display: flex;
|
|
578
|
+
gap: 24px;
|
|
579
|
+
text-align: right;
|
|
580
|
+
}
|
|
348
581
|
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
};
|
|
592
|
+
.session-list-stat-label {
|
|
593
|
+
font-size: 0.7rem;
|
|
594
|
+
color: var(--grey-500);
|
|
595
|
+
text-transform: uppercase;
|
|
596
|
+
}
|
|
382
597
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
394
|
-
function init() {
|
|
395
|
-
fetchStats();
|
|
396
|
-
fetchEvents();
|
|
397
|
-
connectWebSocket();
|
|
609
|
+
.activity-item:last-child { border-bottom: none; }
|
|
398
610
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
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
|
-
|
|
407
|
-
|
|
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>
|