@agent-e/server 1.5.13 → 1.6.2
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/README.md +87 -0
- package/dist/AgentEServer-DT6ETPR6.mjs +7 -0
- package/dist/chunk-53EPMEWX.mjs +1598 -0
- package/dist/chunk-53EPMEWX.mjs.map +1 -0
- package/dist/cli.js +1105 -22
- package/dist/cli.js.map +1 -1
- package/dist/cli.mjs +1 -1
- package/dist/index.d.mts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +1110 -23
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2 -2
- package/package.json +2 -2
- package/dist/AgentEServer-CBG5JMN4.mjs +0 -7
- package/dist/chunk-OXHIY4RU.mjs +0 -515
- package/dist/chunk-OXHIY4RU.mjs.map +0 -1
- /package/dist/{AgentEServer-CBG5JMN4.mjs.map → AgentEServer-DT6ETPR6.mjs.map} +0 -0
package/dist/cli.js
CHANGED
|
@@ -29,7 +29,951 @@ var import_core3 = require("@agent-e/core");
|
|
|
29
29
|
|
|
30
30
|
// src/routes.ts
|
|
31
31
|
var import_core = require("@agent-e/core");
|
|
32
|
+
|
|
33
|
+
// src/dashboard.ts
|
|
34
|
+
function getDashboardHtml() {
|
|
35
|
+
return `<!DOCTYPE html>
|
|
36
|
+
<html lang="en">
|
|
37
|
+
<head>
|
|
38
|
+
<meta charset="UTF-8">
|
|
39
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
40
|
+
<title>AgentE Dashboard</title>
|
|
41
|
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
42
|
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
43
|
+
<link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
|
44
|
+
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
|
|
45
|
+
<style>
|
|
46
|
+
:root {
|
|
47
|
+
--bg-root: #09090b;
|
|
48
|
+
--bg-panel: #18181b;
|
|
49
|
+
--bg-panel-hover: #1f1f23;
|
|
50
|
+
--border: #27272a;
|
|
51
|
+
--border-light: #3f3f46;
|
|
52
|
+
--text-primary: #f4f4f5;
|
|
53
|
+
--text-secondary: #a1a1aa;
|
|
54
|
+
--text-muted: #71717a;
|
|
55
|
+
--text-dim: #52525b;
|
|
56
|
+
--accent: #22c55e;
|
|
57
|
+
--accent-dim: #166534;
|
|
58
|
+
--warning: #eab308;
|
|
59
|
+
--warning-dim: #854d0e;
|
|
60
|
+
--danger: #ef4444;
|
|
61
|
+
--danger-dim: #991b1b;
|
|
62
|
+
--blue: #3b82f6;
|
|
63
|
+
--font-sans: 'IBM Plex Sans', system-ui, sans-serif;
|
|
64
|
+
--font-mono: 'JetBrains Mono', monospace;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
68
|
+
|
|
69
|
+
body {
|
|
70
|
+
background: var(--bg-root);
|
|
71
|
+
color: var(--text-primary);
|
|
72
|
+
font-family: var(--font-sans);
|
|
73
|
+
font-size: 14px;
|
|
74
|
+
line-height: 1.5;
|
|
75
|
+
overflow-x: hidden;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/* \u2500\u2500 Header \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
79
|
+
.header {
|
|
80
|
+
position: sticky;
|
|
81
|
+
top: 0;
|
|
82
|
+
z-index: 100;
|
|
83
|
+
background: var(--bg-root);
|
|
84
|
+
border-bottom: 1px solid var(--border);
|
|
85
|
+
padding: 12px 24px;
|
|
86
|
+
display: flex;
|
|
87
|
+
align-items: center;
|
|
88
|
+
gap: 24px;
|
|
89
|
+
backdrop-filter: blur(8px);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
.header-brand {
|
|
93
|
+
font-weight: 600;
|
|
94
|
+
font-size: 16px;
|
|
95
|
+
color: var(--text-primary);
|
|
96
|
+
white-space: nowrap;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.header-brand span { color: var(--accent); }
|
|
100
|
+
|
|
101
|
+
.kpi-row {
|
|
102
|
+
display: flex;
|
|
103
|
+
gap: 20px;
|
|
104
|
+
flex-wrap: wrap;
|
|
105
|
+
align-items: center;
|
|
106
|
+
margin-left: auto;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
.kpi {
|
|
110
|
+
display: flex;
|
|
111
|
+
align-items: center;
|
|
112
|
+
gap: 6px;
|
|
113
|
+
font-size: 13px;
|
|
114
|
+
color: var(--text-secondary);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
.kpi-value {
|
|
118
|
+
font-family: var(--font-mono);
|
|
119
|
+
font-weight: 500;
|
|
120
|
+
color: var(--text-primary);
|
|
121
|
+
font-size: 13px;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.kpi-value.health-good { color: var(--accent); }
|
|
125
|
+
.kpi-value.health-warn { color: var(--warning); }
|
|
126
|
+
.kpi-value.health-bad { color: var(--danger); }
|
|
127
|
+
|
|
128
|
+
.live-dot {
|
|
129
|
+
width: 8px;
|
|
130
|
+
height: 8px;
|
|
131
|
+
border-radius: 50%;
|
|
132
|
+
background: var(--accent);
|
|
133
|
+
animation: pulse 2s ease-in-out infinite;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.live-dot.disconnected {
|
|
137
|
+
background: var(--danger);
|
|
138
|
+
animation: none;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
@keyframes pulse {
|
|
142
|
+
0%, 100% { opacity: 1; }
|
|
143
|
+
50% { opacity: 0.4; }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/* \u2500\u2500 Layout \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
147
|
+
.container {
|
|
148
|
+
max-width: 1440px;
|
|
149
|
+
margin: 0 auto;
|
|
150
|
+
padding: 20px 24px;
|
|
151
|
+
display: flex;
|
|
152
|
+
flex-direction: column;
|
|
153
|
+
gap: 16px;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
.panel {
|
|
157
|
+
background: var(--bg-panel);
|
|
158
|
+
border: 1px solid var(--border);
|
|
159
|
+
border-radius: 8px;
|
|
160
|
+
padding: 16px;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
.panel-title {
|
|
164
|
+
font-size: 13px;
|
|
165
|
+
font-weight: 600;
|
|
166
|
+
color: var(--text-secondary);
|
|
167
|
+
text-transform: uppercase;
|
|
168
|
+
letter-spacing: 0.05em;
|
|
169
|
+
margin-bottom: 12px;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* \u2500\u2500 Charts grid \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
173
|
+
.charts-grid {
|
|
174
|
+
display: grid;
|
|
175
|
+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
176
|
+
gap: 16px;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.chart-box {
|
|
180
|
+
background: var(--bg-panel);
|
|
181
|
+
border: 1px solid var(--border);
|
|
182
|
+
border-radius: 8px;
|
|
183
|
+
padding: 16px;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.chart-box canvas { width: 100% !important; height: 160px !important; }
|
|
187
|
+
|
|
188
|
+
.chart-label {
|
|
189
|
+
font-size: 12px;
|
|
190
|
+
color: var(--text-muted);
|
|
191
|
+
font-weight: 500;
|
|
192
|
+
margin-bottom: 8px;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
.chart-value {
|
|
196
|
+
font-family: var(--font-mono);
|
|
197
|
+
font-size: 22px;
|
|
198
|
+
font-weight: 500;
|
|
199
|
+
color: var(--text-primary);
|
|
200
|
+
margin-bottom: 8px;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/* \u2500\u2500 Terminal (Decision Feed) \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
204
|
+
.terminal {
|
|
205
|
+
background: var(--bg-root);
|
|
206
|
+
border: 1px solid var(--border);
|
|
207
|
+
border-radius: 8px;
|
|
208
|
+
height: 380px;
|
|
209
|
+
overflow-y: auto;
|
|
210
|
+
font-family: var(--font-mono);
|
|
211
|
+
font-size: 12px;
|
|
212
|
+
line-height: 1.7;
|
|
213
|
+
padding: 12px 16px;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
.terminal::-webkit-scrollbar { width: 6px; }
|
|
217
|
+
.terminal::-webkit-scrollbar-track { background: transparent; }
|
|
218
|
+
.terminal::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
|
|
219
|
+
|
|
220
|
+
.term-line {
|
|
221
|
+
white-space: nowrap;
|
|
222
|
+
opacity: 0;
|
|
223
|
+
transform: translateY(4px);
|
|
224
|
+
animation: termIn 0.3s ease-out forwards;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
@keyframes termIn {
|
|
228
|
+
to { opacity: 1; transform: translateY(0); }
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.t-tick { color: var(--text-dim); }
|
|
232
|
+
.t-ok { color: var(--accent); }
|
|
233
|
+
.t-skip { color: var(--warning); }
|
|
234
|
+
.t-fail { color: var(--danger); }
|
|
235
|
+
.t-principle { color: var(--text-primary); font-weight: 500; }
|
|
236
|
+
.t-param { color: var(--text-secondary); }
|
|
237
|
+
.t-old { color: #d4d4d8; font-variant-numeric: tabular-nums; }
|
|
238
|
+
.t-arrow { color: var(--text-dim); }
|
|
239
|
+
.t-new { color: var(--accent); font-variant-numeric: tabular-nums; }
|
|
240
|
+
.t-meta { color: var(--text-dim); }
|
|
241
|
+
|
|
242
|
+
/* \u2500\u2500 Alerts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
243
|
+
.alerts-container {
|
|
244
|
+
display: flex;
|
|
245
|
+
flex-direction: column;
|
|
246
|
+
gap: 8px;
|
|
247
|
+
max-height: 320px;
|
|
248
|
+
overflow-y: auto;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.alert-card {
|
|
252
|
+
display: flex;
|
|
253
|
+
align-items: flex-start;
|
|
254
|
+
gap: 12px;
|
|
255
|
+
padding: 12px;
|
|
256
|
+
border-radius: 6px;
|
|
257
|
+
border: 1px solid var(--border);
|
|
258
|
+
background: var(--bg-panel);
|
|
259
|
+
transition: opacity 0.3s, transform 0.3s;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
.alert-card.fade-out {
|
|
263
|
+
opacity: 0;
|
|
264
|
+
transform: translateX(20px);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
.alert-severity {
|
|
268
|
+
font-family: var(--font-mono);
|
|
269
|
+
font-weight: 600;
|
|
270
|
+
font-size: 13px;
|
|
271
|
+
padding: 2px 8px;
|
|
272
|
+
border-radius: 4px;
|
|
273
|
+
white-space: nowrap;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
.sev-high { background: var(--danger-dim); color: var(--danger); }
|
|
277
|
+
.sev-med { background: var(--warning-dim); color: var(--warning); }
|
|
278
|
+
.sev-low { background: var(--accent-dim); color: var(--accent); }
|
|
279
|
+
|
|
280
|
+
.alert-body { flex: 1; }
|
|
281
|
+
.alert-principle { font-weight: 500; font-size: 13px; }
|
|
282
|
+
.alert-reason { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }
|
|
283
|
+
|
|
284
|
+
/* \u2500\u2500 Violations table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
285
|
+
.violations-table {
|
|
286
|
+
width: 100%;
|
|
287
|
+
border-collapse: collapse;
|
|
288
|
+
font-size: 12px;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.violations-table th {
|
|
292
|
+
text-align: left;
|
|
293
|
+
color: var(--text-muted);
|
|
294
|
+
font-weight: 500;
|
|
295
|
+
padding: 6px 10px;
|
|
296
|
+
border-bottom: 1px solid var(--border);
|
|
297
|
+
cursor: pointer;
|
|
298
|
+
user-select: none;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
.violations-table th:hover { color: var(--text-secondary); }
|
|
302
|
+
|
|
303
|
+
.violations-table td {
|
|
304
|
+
padding: 6px 10px;
|
|
305
|
+
border-bottom: 1px solid var(--border);
|
|
306
|
+
color: var(--text-secondary);
|
|
307
|
+
font-family: var(--font-mono);
|
|
308
|
+
font-size: 11px;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.violations-table tr:hover td { background: var(--bg-panel-hover); }
|
|
312
|
+
|
|
313
|
+
/* \u2500\u2500 Split row \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
314
|
+
.split-row {
|
|
315
|
+
display: grid;
|
|
316
|
+
grid-template-columns: 1fr 1fr;
|
|
317
|
+
gap: 16px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
@media (max-width: 800px) {
|
|
321
|
+
.split-row { grid-template-columns: 1fr; }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/* \u2500\u2500 Persona bar chart \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
325
|
+
.persona-bars { display: flex; flex-direction: column; gap: 6px; }
|
|
326
|
+
|
|
327
|
+
.persona-row {
|
|
328
|
+
display: flex;
|
|
329
|
+
align-items: center;
|
|
330
|
+
gap: 8px;
|
|
331
|
+
font-size: 12px;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
.persona-label {
|
|
335
|
+
width: 100px;
|
|
336
|
+
text-align: right;
|
|
337
|
+
color: var(--text-secondary);
|
|
338
|
+
font-size: 11px;
|
|
339
|
+
flex-shrink: 0;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
.persona-bar-track {
|
|
343
|
+
flex: 1;
|
|
344
|
+
height: 16px;
|
|
345
|
+
background: var(--bg-root);
|
|
346
|
+
border-radius: 3px;
|
|
347
|
+
overflow: hidden;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
.persona-bar-fill {
|
|
351
|
+
height: 100%;
|
|
352
|
+
background: var(--accent);
|
|
353
|
+
border-radius: 3px;
|
|
354
|
+
transition: width 0.5s ease;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
.persona-pct {
|
|
358
|
+
width: 40px;
|
|
359
|
+
font-family: var(--font-mono);
|
|
360
|
+
font-size: 11px;
|
|
361
|
+
color: var(--text-muted);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/* \u2500\u2500 Registry list \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
365
|
+
.registry-list { display: flex; flex-direction: column; gap: 4px; }
|
|
366
|
+
|
|
367
|
+
.registry-item {
|
|
368
|
+
display: flex;
|
|
369
|
+
justify-content: space-between;
|
|
370
|
+
align-items: center;
|
|
371
|
+
padding: 6px 10px;
|
|
372
|
+
border-radius: 4px;
|
|
373
|
+
font-size: 12px;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
.registry-item:nth-child(odd) { background: rgba(255,255,255,0.02); }
|
|
377
|
+
.registry-key { color: var(--text-secondary); font-family: var(--font-mono); }
|
|
378
|
+
.registry-val { color: var(--accent); font-family: var(--font-mono); font-weight: 500; }
|
|
379
|
+
|
|
380
|
+
/* \u2500\u2500 Advisor mode \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
381
|
+
.advisor-banner {
|
|
382
|
+
display: none;
|
|
383
|
+
background: var(--warning-dim);
|
|
384
|
+
border: 1px solid var(--warning);
|
|
385
|
+
color: var(--warning);
|
|
386
|
+
padding: 8px 16px;
|
|
387
|
+
border-radius: 6px;
|
|
388
|
+
font-size: 13px;
|
|
389
|
+
font-weight: 500;
|
|
390
|
+
align-items: center;
|
|
391
|
+
gap: 8px;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
.advisor-mode .advisor-banner { display: flex; }
|
|
395
|
+
|
|
396
|
+
.pending-pill {
|
|
397
|
+
background: var(--warning);
|
|
398
|
+
color: var(--bg-root);
|
|
399
|
+
font-size: 11px;
|
|
400
|
+
font-weight: 600;
|
|
401
|
+
padding: 1px 8px;
|
|
402
|
+
border-radius: 10px;
|
|
403
|
+
font-family: var(--font-mono);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
.advisor-btn {
|
|
407
|
+
font-family: var(--font-mono);
|
|
408
|
+
font-size: 11px;
|
|
409
|
+
padding: 2px 10px;
|
|
410
|
+
border-radius: 4px;
|
|
411
|
+
border: none;
|
|
412
|
+
cursor: pointer;
|
|
413
|
+
font-weight: 500;
|
|
414
|
+
transition: opacity 0.15s;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
.advisor-btn:hover { opacity: 0.85; }
|
|
418
|
+
.advisor-btn.approve { background: var(--accent); color: var(--bg-root); }
|
|
419
|
+
.advisor-btn.reject { background: var(--danger); color: #fff; }
|
|
420
|
+
|
|
421
|
+
.advisor-actions { display: none; gap: 6px; margin-left: 8px; }
|
|
422
|
+
.advisor-mode .advisor-actions { display: inline-flex; }
|
|
423
|
+
|
|
424
|
+
/* \u2500\u2500 Empty state \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
425
|
+
.empty-state {
|
|
426
|
+
color: var(--text-dim);
|
|
427
|
+
font-size: 13px;
|
|
428
|
+
text-align: center;
|
|
429
|
+
padding: 40px 20px;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/* \u2500\u2500 Reduced motion \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 */
|
|
433
|
+
@media (prefers-reduced-motion: reduce) {
|
|
434
|
+
.term-line { animation: none; opacity: 1; transform: none; }
|
|
435
|
+
.live-dot { animation: none; }
|
|
436
|
+
.persona-bar-fill { transition: none; }
|
|
437
|
+
}
|
|
438
|
+
</style>
|
|
439
|
+
</head>
|
|
440
|
+
<body>
|
|
441
|
+
|
|
442
|
+
<!-- Header -->
|
|
443
|
+
<div class="header" id="header">
|
|
444
|
+
<div class="header-brand">Agent<span>E</span> v1.6</div>
|
|
445
|
+
<div class="kpi-row">
|
|
446
|
+
<div class="kpi">Health <span class="kpi-value health-good" id="kpi-health">--</span></div>
|
|
447
|
+
<div class="kpi">Mode <span class="kpi-value" id="kpi-mode">--</span></div>
|
|
448
|
+
<div class="kpi">Tick <span class="kpi-value" id="kpi-tick">0</span></div>
|
|
449
|
+
<div class="kpi">Uptime <span class="kpi-value" id="kpi-uptime">0s</span></div>
|
|
450
|
+
<div class="kpi">Plans <span class="kpi-value" id="kpi-plans">0</span></div>
|
|
451
|
+
<div class="live-dot" id="live-dot" title="WebSocket connected"></div>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div class="container" id="app">
|
|
456
|
+
<!-- Advisor banner -->
|
|
457
|
+
<div class="advisor-banner" id="advisor-banner">
|
|
458
|
+
ADVISOR MODE \u2014 Recommendations require manual approval
|
|
459
|
+
<span class="pending-pill" id="pending-count">0</span> pending
|
|
460
|
+
</div>
|
|
461
|
+
|
|
462
|
+
<!-- Charts -->
|
|
463
|
+
<div class="charts-grid">
|
|
464
|
+
<div class="chart-box">
|
|
465
|
+
<div class="chart-label">Economy Health</div>
|
|
466
|
+
<div class="chart-value" id="cv-health">--</div>
|
|
467
|
+
<canvas id="chart-health"></canvas>
|
|
468
|
+
</div>
|
|
469
|
+
<div class="chart-box">
|
|
470
|
+
<div class="chart-label">Gini Coefficient</div>
|
|
471
|
+
<div class="chart-value" id="cv-gini">--</div>
|
|
472
|
+
<canvas id="chart-gini"></canvas>
|
|
473
|
+
</div>
|
|
474
|
+
<div class="chart-box">
|
|
475
|
+
<div class="chart-label">Net Flow</div>
|
|
476
|
+
<div class="chart-value" id="cv-netflow">--</div>
|
|
477
|
+
<canvas id="chart-netflow"></canvas>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="chart-box">
|
|
480
|
+
<div class="chart-label">Avg Satisfaction</div>
|
|
481
|
+
<div class="chart-value" id="cv-satisfaction">--</div>
|
|
482
|
+
<canvas id="chart-satisfaction"></canvas>
|
|
483
|
+
</div>
|
|
484
|
+
</div>
|
|
485
|
+
|
|
486
|
+
<!-- Decision Feed -->
|
|
487
|
+
<div class="panel">
|
|
488
|
+
<div class="panel-title">Decision Feed</div>
|
|
489
|
+
<div class="terminal" id="terminal"></div>
|
|
490
|
+
</div>
|
|
491
|
+
|
|
492
|
+
<!-- Active Alerts -->
|
|
493
|
+
<div class="panel">
|
|
494
|
+
<div class="panel-title">Active Alerts</div>
|
|
495
|
+
<div class="alerts-container" id="alerts-container">
|
|
496
|
+
<div class="empty-state" id="alerts-empty">No active violations</div>
|
|
497
|
+
</div>
|
|
498
|
+
</div>
|
|
499
|
+
|
|
500
|
+
<!-- Violation History -->
|
|
501
|
+
<div class="panel">
|
|
502
|
+
<div class="panel-title">Violation History</div>
|
|
503
|
+
<div style="max-height:320px;overflow-y:auto">
|
|
504
|
+
<table class="violations-table" id="violations-table">
|
|
505
|
+
<thead>
|
|
506
|
+
<tr>
|
|
507
|
+
<th data-sort="tick">Tick</th>
|
|
508
|
+
<th data-sort="principle">Principle</th>
|
|
509
|
+
<th data-sort="severity">Severity</th>
|
|
510
|
+
<th data-sort="parameter">Parameter</th>
|
|
511
|
+
<th data-sort="result">Result</th>
|
|
512
|
+
</tr>
|
|
513
|
+
</thead>
|
|
514
|
+
<tbody id="violations-body"></tbody>
|
|
515
|
+
</table>
|
|
516
|
+
</div>
|
|
517
|
+
</div>
|
|
518
|
+
|
|
519
|
+
<!-- Split: Personas + Registry -->
|
|
520
|
+
<div class="split-row">
|
|
521
|
+
<div class="panel">
|
|
522
|
+
<div class="panel-title">Persona Distribution</div>
|
|
523
|
+
<div class="persona-bars" id="persona-bars">
|
|
524
|
+
<div class="empty-state">No persona data yet</div>
|
|
525
|
+
</div>
|
|
526
|
+
</div>
|
|
527
|
+
<div class="panel">
|
|
528
|
+
<div class="panel-title">Parameter Registry</div>
|
|
529
|
+
<div class="registry-list" id="registry-list">
|
|
530
|
+
<div class="empty-state">No parameters registered</div>
|
|
531
|
+
</div>
|
|
532
|
+
</div>
|
|
533
|
+
</div>
|
|
534
|
+
</div>
|
|
535
|
+
|
|
536
|
+
<script>
|
|
537
|
+
(function() {
|
|
538
|
+
'use strict';
|
|
539
|
+
|
|
540
|
+
// \u2500\u2500 State \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
541
|
+
let ws = null;
|
|
542
|
+
let reconnectDelay = 1000;
|
|
543
|
+
const MAX_RECONNECT = 30000;
|
|
544
|
+
let isAdvisor = false;
|
|
545
|
+
let pendingDecisions = [];
|
|
546
|
+
const MAX_TERMINAL_LINES = 80;
|
|
547
|
+
const MAX_VIOLATIONS = 100;
|
|
548
|
+
let violationSortKey = 'tick';
|
|
549
|
+
let violationSortAsc = false;
|
|
550
|
+
let violations = [];
|
|
551
|
+
|
|
552
|
+
// Chart instances
|
|
553
|
+
let chartHealth, chartGini, chartNetflow, chartSatisfaction;
|
|
554
|
+
|
|
555
|
+
// \u2500\u2500 DOM refs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
556
|
+
const $kpiHealth = document.getElementById('kpi-health');
|
|
557
|
+
const $kpiMode = document.getElementById('kpi-mode');
|
|
558
|
+
const $kpiTick = document.getElementById('kpi-tick');
|
|
559
|
+
const $kpiUptime = document.getElementById('kpi-uptime');
|
|
560
|
+
const $kpiPlans = document.getElementById('kpi-plans');
|
|
561
|
+
const $liveDot = document.getElementById('live-dot');
|
|
562
|
+
const $terminal = document.getElementById('terminal');
|
|
563
|
+
const $alertsContainer = document.getElementById('alerts-container');
|
|
564
|
+
const $alertsEmpty = document.getElementById('alerts-empty');
|
|
565
|
+
const $violationsBody = document.getElementById('violations-body');
|
|
566
|
+
const $personaBars = document.getElementById('persona-bars');
|
|
567
|
+
const $registryList = document.getElementById('registry-list');
|
|
568
|
+
const $pendingCount = document.getElementById('pending-count');
|
|
569
|
+
const $app = document.getElementById('app');
|
|
570
|
+
|
|
571
|
+
// \u2500\u2500 Helpers \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
572
|
+
function esc(s) { return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"').replace(/'/g,'''); }
|
|
573
|
+
function pad(n, w) { return String(n).padStart(w || 4, ' '); }
|
|
574
|
+
function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\u2014'; }
|
|
575
|
+
function pct(n) { return typeof n === 'number' ? (n * 100).toFixed(0) + '%' : '\u2014'; }
|
|
576
|
+
|
|
577
|
+
function formatUptime(ms) {
|
|
578
|
+
const s = Math.floor(ms / 1000);
|
|
579
|
+
if (s < 60) return s + 's';
|
|
580
|
+
if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
|
|
581
|
+
const h = Math.floor(s / 3600);
|
|
582
|
+
return h + 'h ' + Math.floor((s % 3600) / 60) + 'm';
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
function healthClass(h) {
|
|
586
|
+
if (h >= 70) return 'health-good';
|
|
587
|
+
if (h >= 40) return 'health-warn';
|
|
588
|
+
return 'health-bad';
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
function sevClass(s) {
|
|
592
|
+
if (s >= 7) return 'sev-high';
|
|
593
|
+
if (s >= 4) return 'sev-med';
|
|
594
|
+
return 'sev-low';
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// \u2500\u2500 Chart setup \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
598
|
+
const chartOpts = {
|
|
599
|
+
responsive: true,
|
|
600
|
+
maintainAspectRatio: false,
|
|
601
|
+
animation: { duration: 300 },
|
|
602
|
+
plugins: { legend: { display: false } },
|
|
603
|
+
scales: {
|
|
604
|
+
x: { display: false },
|
|
605
|
+
y: {
|
|
606
|
+
ticks: { color: '#71717a', font: { family: "'JetBrains Mono'", size: 10 } },
|
|
607
|
+
grid: { color: 'rgba(63,63,70,0.3)' },
|
|
608
|
+
border: { display: false },
|
|
609
|
+
}
|
|
610
|
+
},
|
|
611
|
+
elements: {
|
|
612
|
+
point: { radius: 0 },
|
|
613
|
+
line: { borderWidth: 1.5, tension: 0.3 },
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
|
|
617
|
+
function makeChart(id, color, minY, maxY) {
|
|
618
|
+
const ctx = document.getElementById(id).getContext('2d');
|
|
619
|
+
const opts = JSON.parse(JSON.stringify(chartOpts));
|
|
620
|
+
if (minY !== undefined) opts.scales.y.min = minY;
|
|
621
|
+
if (maxY !== undefined) opts.scales.y.max = maxY;
|
|
622
|
+
return new Chart(ctx, {
|
|
623
|
+
type: 'line',
|
|
624
|
+
data: {
|
|
625
|
+
labels: [],
|
|
626
|
+
datasets: [{
|
|
627
|
+
data: [],
|
|
628
|
+
borderColor: color,
|
|
629
|
+
backgroundColor: color + '18',
|
|
630
|
+
fill: true,
|
|
631
|
+
}]
|
|
632
|
+
},
|
|
633
|
+
options: opts,
|
|
634
|
+
});
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function initCharts() {
|
|
638
|
+
chartHealth = makeChart('chart-health', '#22c55e', 0, 100);
|
|
639
|
+
chartGini = makeChart('chart-gini', '#eab308', 0, 1);
|
|
640
|
+
chartNetflow = makeChart('chart-netflow', '#3b82f6');
|
|
641
|
+
chartSatisfaction = makeChart('chart-satisfaction', '#22c55e', 0, 100);
|
|
642
|
+
}
|
|
643
|
+
|
|
644
|
+
function updateChart(chart, labels, data) {
|
|
645
|
+
chart.data.labels = labels;
|
|
646
|
+
chart.data.datasets[0].data = data;
|
|
647
|
+
chart.update('none');
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// \u2500\u2500 Terminal \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
651
|
+
function addTerminalLine(html) {
|
|
652
|
+
const el = document.createElement('div');
|
|
653
|
+
el.className = 'term-line';
|
|
654
|
+
el.innerHTML = html;
|
|
655
|
+
$terminal.appendChild(el);
|
|
656
|
+
while ($terminal.children.length > MAX_TERMINAL_LINES) {
|
|
657
|
+
$terminal.removeChild($terminal.firstChild);
|
|
658
|
+
}
|
|
659
|
+
$terminal.scrollTop = $terminal.scrollHeight;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
function decisionToTerminal(d) {
|
|
663
|
+
const resultIcon = d.result === 'applied'
|
|
664
|
+
? '<span class="t-ok">\\u2705 </span>'
|
|
665
|
+
: d.result === 'rejected'
|
|
666
|
+
? '<span class="t-fail">\\u274c </span>'
|
|
667
|
+
: '<span class="t-skip">\\u23f8 </span>';
|
|
668
|
+
|
|
669
|
+
const principle = d.diagnosis?.principle || {};
|
|
670
|
+
const plan = d.plan || {};
|
|
671
|
+
const severity = d.diagnosis?.violation?.severity ?? '?';
|
|
672
|
+
const confidence = d.diagnosis?.violation?.confidence;
|
|
673
|
+
const confStr = confidence != null ? (confidence * 100).toFixed(0) + '%' : '?';
|
|
674
|
+
|
|
675
|
+
let advisorBtns = '';
|
|
676
|
+
if (isAdvisor && d.result === 'skipped_override') {
|
|
677
|
+
advisorBtns = '<span class="advisor-actions">'
|
|
678
|
+
+ '<button class="advisor-btn approve" onclick="window._approve(\\'' + esc(d.id) + '\\')">[Approve]</button>'
|
|
679
|
+
+ '<button class="advisor-btn reject" onclick="window._reject(\\'' + esc(d.id) + '\\')">[Reject]</button>'
|
|
680
|
+
+ '</span>';
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
return '<span class="t-tick">[Tick ' + pad(d.tick) + ']</span> '
|
|
684
|
+
+ resultIcon
|
|
685
|
+
+ '<span class="t-principle">[' + esc(principle.id || '?') + '] ' + esc(principle.name || '') + ':</span> '
|
|
686
|
+
+ '<span class="t-param">' + esc(plan.parameter || '\u2014') + ' </span>'
|
|
687
|
+
+ '<span class="t-old">' + fmt(plan.currentValue) + '</span>'
|
|
688
|
+
+ '<span class="t-arrow"> \\u2192 </span>'
|
|
689
|
+
+ '<span class="t-new">' + fmt(plan.targetValue) + '</span>'
|
|
690
|
+
+ '<span class="t-meta"> severity ' + severity + '/10, confidence ' + confStr + '</span>'
|
|
691
|
+
+ advisorBtns;
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
// \u2500\u2500 Alerts \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
695
|
+
function renderAlerts(alerts) {
|
|
696
|
+
if (!alerts || alerts.length === 0) {
|
|
697
|
+
$alertsContainer.innerHTML = '<div class="empty-state">No active violations</div>';
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
const sorted = [...alerts].sort((a, b) => (b.severity || 0) - (a.severity || 0));
|
|
701
|
+
$alertsContainer.innerHTML = sorted.map(function(a) {
|
|
702
|
+
const sev = a.severity || a.violation?.severity || 0;
|
|
703
|
+
const sc = sevClass(sev);
|
|
704
|
+
const name = a.principleName || a.principle?.name || '?';
|
|
705
|
+
const pid = a.principleId || a.principle?.id || '?';
|
|
706
|
+
const reason = a.reasoning || a.violation?.suggestedAction?.reasoning || '';
|
|
707
|
+
return '<div class="alert-card">'
|
|
708
|
+
+ '<span class="alert-severity ' + sc + '">' + sev + '/10</span>'
|
|
709
|
+
+ '<div class="alert-body">'
|
|
710
|
+
+ '<div class="alert-principle">[' + esc(pid) + '] ' + esc(name) + '</div>'
|
|
711
|
+
+ '<div class="alert-reason">' + esc(reason) + '</div>'
|
|
712
|
+
+ '</div></div>';
|
|
713
|
+
}).join('');
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// \u2500\u2500 Violations table \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
717
|
+
function addViolation(d) {
|
|
718
|
+
violations.push({
|
|
719
|
+
tick: d.tick,
|
|
720
|
+
principle: (d.diagnosis?.principle?.id || '?') + ' ' + (d.diagnosis?.principle?.name || ''),
|
|
721
|
+
severity: d.diagnosis?.violation?.severity || 0,
|
|
722
|
+
parameter: d.plan?.parameter || '\u2014',
|
|
723
|
+
result: d.result,
|
|
724
|
+
});
|
|
725
|
+
if (violations.length > MAX_VIOLATIONS) violations.shift();
|
|
726
|
+
renderViolations();
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
function renderViolations() {
|
|
730
|
+
const sorted = [...violations].sort(function(a, b) {
|
|
731
|
+
const va = a[violationSortKey], vb = b[violationSortKey];
|
|
732
|
+
if (va < vb) return violationSortAsc ? -1 : 1;
|
|
733
|
+
if (va > vb) return violationSortAsc ? 1 : -1;
|
|
734
|
+
return 0;
|
|
735
|
+
});
|
|
736
|
+
$violationsBody.innerHTML = sorted.map(function(v) {
|
|
737
|
+
return '<tr>'
|
|
738
|
+
+ '<td>' + v.tick + '</td>'
|
|
739
|
+
+ '<td style="color:var(--text-primary);font-family:var(--font-sans)">' + esc(v.principle) + '</td>'
|
|
740
|
+
+ '<td><span class="alert-severity ' + sevClass(v.severity) + '">' + v.severity + '</span></td>'
|
|
741
|
+
+ '<td>' + esc(v.parameter) + '</td>'
|
|
742
|
+
+ '<td>' + esc(v.result) + '</td>'
|
|
743
|
+
+ '</tr>';
|
|
744
|
+
}).join('');
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
// Table sorting
|
|
748
|
+
document.querySelectorAll('.violations-table th').forEach(function(th) {
|
|
749
|
+
th.addEventListener('click', function() {
|
|
750
|
+
const key = th.dataset.sort;
|
|
751
|
+
if (violationSortKey === key) violationSortAsc = !violationSortAsc;
|
|
752
|
+
else { violationSortKey = key; violationSortAsc = true; }
|
|
753
|
+
renderViolations();
|
|
754
|
+
});
|
|
755
|
+
});
|
|
756
|
+
|
|
757
|
+
// \u2500\u2500 Personas \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
758
|
+
function renderPersonas(dist) {
|
|
759
|
+
if (!dist || Object.keys(dist).length === 0) {
|
|
760
|
+
$personaBars.innerHTML = '<div class="empty-state">No persona data yet</div>';
|
|
761
|
+
return;
|
|
762
|
+
}
|
|
763
|
+
const total = Object.values(dist).reduce(function(s, v) { return s + v; }, 0);
|
|
764
|
+
const entries = Object.entries(dist).sort(function(a, b) { return b[1] - a[1]; });
|
|
765
|
+
$personaBars.innerHTML = entries.map(function(e) {
|
|
766
|
+
const pctVal = total > 0 ? (e[1] / total * 100) : 0;
|
|
767
|
+
return '<div class="persona-row">'
|
|
768
|
+
+ '<div class="persona-label">' + esc(e[0]) + '</div>'
|
|
769
|
+
+ '<div class="persona-bar-track"><div class="persona-bar-fill" style="width:' + pctVal + '%"></div></div>'
|
|
770
|
+
+ '<div class="persona-pct">' + pctVal.toFixed(0) + '%</div>'
|
|
771
|
+
+ '</div>';
|
|
772
|
+
}).join('');
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// \u2500\u2500 Registry \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
776
|
+
function renderRegistry(principles) {
|
|
777
|
+
if (!principles || principles.length === 0) {
|
|
778
|
+
$registryList.innerHTML = '<div class="empty-state">No parameters registered</div>';
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
$registryList.innerHTML = principles.slice(0, 30).map(function(p) {
|
|
782
|
+
return '<div class="registry-item">'
|
|
783
|
+
+ '<span class="registry-key">[' + esc(p.id) + ']</span>'
|
|
784
|
+
+ '<span class="registry-val">' + esc(p.name) + '</span>'
|
|
785
|
+
+ '</div>';
|
|
786
|
+
}).join('');
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
// \u2500\u2500 KPI update \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
790
|
+
function updateKPIs(data) {
|
|
791
|
+
if (data.health != null) {
|
|
792
|
+
$kpiHealth.textContent = data.health + '/100';
|
|
793
|
+
$kpiHealth.className = 'kpi-value ' + healthClass(data.health);
|
|
794
|
+
document.getElementById('cv-health').textContent = data.health + '/100';
|
|
795
|
+
}
|
|
796
|
+
if (data.mode != null) {
|
|
797
|
+
$kpiMode.textContent = data.mode;
|
|
798
|
+
isAdvisor = data.mode === 'advisor';
|
|
799
|
+
$app.classList.toggle('advisor-mode', isAdvisor);
|
|
800
|
+
}
|
|
801
|
+
if (data.tick != null) $kpiTick.textContent = data.tick;
|
|
802
|
+
if (data.uptime != null) $kpiUptime.textContent = formatUptime(data.uptime);
|
|
803
|
+
if (data.activePlans != null) $kpiPlans.textContent = data.activePlans;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
// \u2500\u2500 Metrics history \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
807
|
+
function updateChartsFromHistory(history) {
|
|
808
|
+
if (!history || history.length === 0) return;
|
|
809
|
+
const ticks = history.map(function(h) { return h.tick; });
|
|
810
|
+
updateChart(chartHealth, ticks, history.map(function(h) { return h.health; }));
|
|
811
|
+
updateChart(chartGini, ticks, history.map(function(h) { return h.giniCoefficient; }));
|
|
812
|
+
updateChart(chartNetflow, ticks, history.map(function(h) { return h.netFlow; }));
|
|
813
|
+
updateChart(chartSatisfaction, ticks, history.map(function(h) { return h.avgSatisfaction; }));
|
|
814
|
+
|
|
815
|
+
const last = history[history.length - 1];
|
|
816
|
+
document.getElementById('cv-gini').textContent = last.giniCoefficient.toFixed(3);
|
|
817
|
+
document.getElementById('cv-netflow').textContent = last.netFlow.toFixed(1);
|
|
818
|
+
document.getElementById('cv-satisfaction').textContent = last.avgSatisfaction.toFixed(0) + '/100';
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// \u2500\u2500 API calls \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
822
|
+
function fetchJSON(path) {
|
|
823
|
+
return fetch(path).then(function(r) { return r.json(); });
|
|
824
|
+
}
|
|
825
|
+
|
|
826
|
+
function postJSON(path, body) {
|
|
827
|
+
return fetch(path, {
|
|
828
|
+
method: 'POST',
|
|
829
|
+
headers: { 'Content-Type': 'application/json' },
|
|
830
|
+
body: JSON.stringify(body),
|
|
831
|
+
}).then(function(r) { return r.json(); });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
function loadInitialData() {
|
|
835
|
+
fetchJSON('/health').then(function(data) {
|
|
836
|
+
updateKPIs(data);
|
|
837
|
+
}).catch(function() {});
|
|
838
|
+
|
|
839
|
+
fetchJSON('/decisions?limit=50').then(function(data) {
|
|
840
|
+
if (data.decisions) {
|
|
841
|
+
data.decisions.reverse().forEach(function(d) {
|
|
842
|
+
addTerminalLine(decisionToTerminal(d));
|
|
843
|
+
addViolation(d);
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
}).catch(function() {});
|
|
847
|
+
|
|
848
|
+
fetchJSON('/metrics').then(function(data) {
|
|
849
|
+
if (data.history) updateChartsFromHistory(data.history);
|
|
850
|
+
if (data.latest) {
|
|
851
|
+
renderPersonas(data.latest.personaDistribution);
|
|
852
|
+
}
|
|
853
|
+
}).catch(function() {});
|
|
854
|
+
|
|
855
|
+
fetchJSON('/principles').then(function(data) {
|
|
856
|
+
if (data.principles) renderRegistry(data.principles);
|
|
857
|
+
}).catch(function() {});
|
|
858
|
+
|
|
859
|
+
fetchJSON('/pending').then(function(data) {
|
|
860
|
+
if (data.pending) {
|
|
861
|
+
pendingDecisions = data.pending;
|
|
862
|
+
$pendingCount.textContent = data.count || 0;
|
|
863
|
+
}
|
|
864
|
+
}).catch(function() {});
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// \u2500\u2500 Polling fallback \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
868
|
+
let pollInterval = null;
|
|
869
|
+
|
|
870
|
+
function startPolling() {
|
|
871
|
+
if (pollInterval) return;
|
|
872
|
+
pollInterval = setInterval(function() {
|
|
873
|
+
fetchJSON('/health').then(updateKPIs).catch(function() {});
|
|
874
|
+
fetchJSON('/metrics').then(function(data) {
|
|
875
|
+
if (data.history) updateChartsFromHistory(data.history);
|
|
876
|
+
if (data.latest) renderPersonas(data.latest.personaDistribution);
|
|
877
|
+
}).catch(function() {});
|
|
878
|
+
}, 5000);
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
function stopPolling() {
|
|
882
|
+
if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
// \u2500\u2500 WebSocket \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
886
|
+
function connectWS() {
|
|
887
|
+
const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
888
|
+
ws = new WebSocket(proto + '//' + location.host);
|
|
889
|
+
|
|
890
|
+
ws.onopen = function() {
|
|
891
|
+
reconnectDelay = 1000;
|
|
892
|
+
$liveDot.classList.remove('disconnected');
|
|
893
|
+
$liveDot.title = 'WebSocket connected';
|
|
894
|
+
stopPolling();
|
|
895
|
+
// Request fresh health
|
|
896
|
+
ws.send(JSON.stringify({ type: 'health' }));
|
|
897
|
+
};
|
|
898
|
+
|
|
899
|
+
ws.onclose = function() {
|
|
900
|
+
$liveDot.classList.add('disconnected');
|
|
901
|
+
$liveDot.title = 'WebSocket disconnected \u2014 reconnecting...';
|
|
902
|
+
startPolling();
|
|
903
|
+
setTimeout(connectWS, reconnectDelay);
|
|
904
|
+
reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT);
|
|
905
|
+
};
|
|
906
|
+
|
|
907
|
+
ws.onerror = function() { ws.close(); };
|
|
908
|
+
|
|
909
|
+
ws.onmessage = function(ev) {
|
|
910
|
+
var msg;
|
|
911
|
+
try { msg = JSON.parse(ev.data); } catch(e) { return; }
|
|
912
|
+
|
|
913
|
+
switch (msg.type) {
|
|
914
|
+
case 'tick_result':
|
|
915
|
+
updateKPIs({ health: msg.health, tick: msg.tick });
|
|
916
|
+
if (msg.alerts) renderAlerts(msg.alerts);
|
|
917
|
+
// Refresh charts
|
|
918
|
+
fetchJSON('/metrics').then(function(data) {
|
|
919
|
+
if (data.history) updateChartsFromHistory(data.history);
|
|
920
|
+
if (data.latest) renderPersonas(data.latest.personaDistribution);
|
|
921
|
+
}).catch(function() {});
|
|
922
|
+
break;
|
|
923
|
+
|
|
924
|
+
case 'health_result':
|
|
925
|
+
updateKPIs(msg);
|
|
926
|
+
break;
|
|
927
|
+
|
|
928
|
+
case 'advisor_action':
|
|
929
|
+
if (msg.action === 'approved' || msg.action === 'rejected') {
|
|
930
|
+
pendingDecisions = pendingDecisions.filter(function(d) {
|
|
931
|
+
return d.id !== msg.decisionId;
|
|
932
|
+
});
|
|
933
|
+
$pendingCount.textContent = pendingDecisions.length;
|
|
934
|
+
}
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// \u2500\u2500 Advisor actions \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
941
|
+
window._approve = function(id) {
|
|
942
|
+
postJSON('/approve', { decisionId: id }).then(function(data) {
|
|
943
|
+
if (data.ok) {
|
|
944
|
+
addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-ok">\\u2705 Approved ' + id + '</span>');
|
|
945
|
+
}
|
|
946
|
+
}).catch(function() {});
|
|
947
|
+
};
|
|
948
|
+
|
|
949
|
+
window._reject = function(id) {
|
|
950
|
+
var reason = prompt('Rejection reason (optional):');
|
|
951
|
+
postJSON('/reject', { decisionId: id, reason: reason || undefined }).then(function(data) {
|
|
952
|
+
if (data.ok) {
|
|
953
|
+
addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-fail">\\u274c Rejected ' + id + '</span>');
|
|
954
|
+
}
|
|
955
|
+
}).catch(function() {});
|
|
956
|
+
};
|
|
957
|
+
|
|
958
|
+
// \u2500\u2500 Init \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
959
|
+
initCharts();
|
|
960
|
+
loadInitialData();
|
|
961
|
+
connectWS();
|
|
962
|
+
|
|
963
|
+
})();
|
|
964
|
+
</script>
|
|
965
|
+
</body>
|
|
966
|
+
</html>`;
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
// src/routes.ts
|
|
970
|
+
function setSecurityHeaders(res) {
|
|
971
|
+
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
972
|
+
res.setHeader("X-Frame-Options", "DENY");
|
|
973
|
+
res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
|
|
974
|
+
}
|
|
32
975
|
function setCorsHeaders(res, origin) {
|
|
976
|
+
setSecurityHeaders(res);
|
|
33
977
|
res.setHeader("Access-Control-Allow-Origin", origin);
|
|
34
978
|
res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
|
|
35
979
|
res.setHeader("Access-Control-Allow-Headers", "Content-Type");
|
|
@@ -86,21 +1030,19 @@ function createRouteHandler(server2) {
|
|
|
86
1030
|
const payload = parsed;
|
|
87
1031
|
const state = payload["state"] ?? parsed;
|
|
88
1032
|
const events = payload["events"];
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
return;
|
|
97
|
-
}
|
|
1033
|
+
const validation = server2.validateState ? (0, import_core.validateEconomyState)(state) : null;
|
|
1034
|
+
if (validation && !validation.valid) {
|
|
1035
|
+
json(res, 400, {
|
|
1036
|
+
error: "invalid_state",
|
|
1037
|
+
validationErrors: validation.errors
|
|
1038
|
+
}, cors);
|
|
1039
|
+
return;
|
|
98
1040
|
}
|
|
99
1041
|
const result = await server2.processTick(
|
|
100
1042
|
state,
|
|
101
1043
|
Array.isArray(events) ? events : void 0
|
|
102
1044
|
);
|
|
103
|
-
const warnings =
|
|
1045
|
+
const warnings = validation?.warnings ?? [];
|
|
104
1046
|
json(res, 200, {
|
|
105
1047
|
adjustments: result.adjustments,
|
|
106
1048
|
alerts: result.alerts.map((a) => ({
|
|
@@ -128,12 +1070,18 @@ function createRouteHandler(server2) {
|
|
|
128
1070
|
return;
|
|
129
1071
|
}
|
|
130
1072
|
if (path === "/decisions" && method === "GET") {
|
|
131
|
-
const
|
|
132
|
-
const
|
|
1073
|
+
const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
|
|
1074
|
+
const limit = Math.min(Math.max(isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
|
|
1075
|
+
const sinceParam = url.searchParams.get("since");
|
|
133
1076
|
const agentE = server2.getAgentE();
|
|
134
1077
|
let decisions;
|
|
135
|
-
if (
|
|
136
|
-
|
|
1078
|
+
if (sinceParam) {
|
|
1079
|
+
const since = parseInt(sinceParam, 10);
|
|
1080
|
+
if (isNaN(since)) {
|
|
1081
|
+
json(res, 400, { error: 'Invalid "since" parameter \u2014 must be a number' }, cors);
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
decisions = agentE.getDecisions({ since });
|
|
137
1085
|
} else {
|
|
138
1086
|
decisions = agentE.log.latest(limit);
|
|
139
1087
|
}
|
|
@@ -164,6 +1112,14 @@ function createRouteHandler(server2) {
|
|
|
164
1112
|
for (const c of config["constrain"]) {
|
|
165
1113
|
if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
|
|
166
1114
|
const constraint = c;
|
|
1115
|
+
if (!isFinite(constraint.min) || !isFinite(constraint.max)) {
|
|
1116
|
+
json(res, 400, { error: "Constraint bounds must be finite numbers" }, cors);
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (constraint.min > constraint.max) {
|
|
1120
|
+
json(res, 400, { error: "Constraint min cannot exceed max" }, cors);
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
167
1123
|
server2.constrain(constraint.param, { min: constraint.min, max: constraint.max });
|
|
168
1124
|
}
|
|
169
1125
|
}
|
|
@@ -218,6 +1174,112 @@ function createRouteHandler(server2) {
|
|
|
218
1174
|
}, cors);
|
|
219
1175
|
return;
|
|
220
1176
|
}
|
|
1177
|
+
if (path === "/" && method === "GET" && server2.serveDashboard) {
|
|
1178
|
+
setCorsHeaders(res, cors);
|
|
1179
|
+
res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'unsafe-inline' https://cdn.jsdelivr.net; style-src 'unsafe-inline' https://fonts.googleapis.com; font-src https://fonts.gstatic.com; connect-src 'self' ws: wss:; img-src 'self' data:");
|
|
1180
|
+
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
1181
|
+
res.end(getDashboardHtml());
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
if (path === "/metrics" && method === "GET") {
|
|
1185
|
+
const agentE = server2.getAgentE();
|
|
1186
|
+
const latest = agentE.store.latest();
|
|
1187
|
+
const history = agentE.store.recentHistory(100);
|
|
1188
|
+
json(res, 200, { latest, history }, cors);
|
|
1189
|
+
return;
|
|
1190
|
+
}
|
|
1191
|
+
if (path === "/metrics/personas" && method === "GET") {
|
|
1192
|
+
const agentE = server2.getAgentE();
|
|
1193
|
+
const latest = agentE.store.latest();
|
|
1194
|
+
const dist = latest.personaDistribution || {};
|
|
1195
|
+
const total = Object.values(dist).reduce((s, v) => s + v, 0);
|
|
1196
|
+
json(res, 200, { distribution: dist, total }, cors);
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
if (path === "/approve" && method === "POST") {
|
|
1200
|
+
const body = await readBody(req);
|
|
1201
|
+
let parsed;
|
|
1202
|
+
try {
|
|
1203
|
+
parsed = JSON.parse(body);
|
|
1204
|
+
} catch {
|
|
1205
|
+
json(res, 400, { error: "Invalid JSON" }, cors);
|
|
1206
|
+
return;
|
|
1207
|
+
}
|
|
1208
|
+
const payload = parsed;
|
|
1209
|
+
const decisionId = payload["decisionId"];
|
|
1210
|
+
if (!decisionId) {
|
|
1211
|
+
json(res, 400, { error: "missing_decision_id" }, cors);
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
const agentE = server2.getAgentE();
|
|
1215
|
+
if (agentE.getMode() !== "advisor") {
|
|
1216
|
+
json(res, 400, { error: "not_in_advisor_mode" }, cors);
|
|
1217
|
+
return;
|
|
1218
|
+
}
|
|
1219
|
+
const entry = agentE.log.getById(decisionId);
|
|
1220
|
+
if (!entry) {
|
|
1221
|
+
json(res, 404, { error: "decision_not_found" }, cors);
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
if (entry.result !== "skipped_override") {
|
|
1225
|
+
json(res, 409, { error: "decision_not_pending", currentResult: entry.result }, cors);
|
|
1226
|
+
return;
|
|
1227
|
+
}
|
|
1228
|
+
await agentE.apply(entry.plan);
|
|
1229
|
+
agentE.log.updateResult(decisionId, "applied");
|
|
1230
|
+
server2.broadcast({ type: "advisor_action", action: "approved", decisionId });
|
|
1231
|
+
json(res, 200, {
|
|
1232
|
+
ok: true,
|
|
1233
|
+
parameter: entry.plan.parameter,
|
|
1234
|
+
value: entry.plan.targetValue
|
|
1235
|
+
}, cors);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
if (path === "/reject" && method === "POST") {
|
|
1239
|
+
const body = await readBody(req);
|
|
1240
|
+
let parsed;
|
|
1241
|
+
try {
|
|
1242
|
+
parsed = JSON.parse(body);
|
|
1243
|
+
} catch {
|
|
1244
|
+
json(res, 400, { error: "Invalid JSON" }, cors);
|
|
1245
|
+
return;
|
|
1246
|
+
}
|
|
1247
|
+
const payload = parsed;
|
|
1248
|
+
const decisionId = payload["decisionId"];
|
|
1249
|
+
const reason = payload["reason"] || void 0;
|
|
1250
|
+
if (!decisionId) {
|
|
1251
|
+
json(res, 400, { error: "missing_decision_id" }, cors);
|
|
1252
|
+
return;
|
|
1253
|
+
}
|
|
1254
|
+
const agentE = server2.getAgentE();
|
|
1255
|
+
if (agentE.getMode() !== "advisor") {
|
|
1256
|
+
json(res, 400, { error: "not_in_advisor_mode" }, cors);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const entry = agentE.log.getById(decisionId);
|
|
1260
|
+
if (!entry) {
|
|
1261
|
+
json(res, 404, { error: "decision_not_found" }, cors);
|
|
1262
|
+
return;
|
|
1263
|
+
}
|
|
1264
|
+
if (entry.result !== "skipped_override") {
|
|
1265
|
+
json(res, 409, { error: "decision_not_pending", currentResult: entry.result }, cors);
|
|
1266
|
+
return;
|
|
1267
|
+
}
|
|
1268
|
+
agentE.log.updateResult(decisionId, "rejected", reason);
|
|
1269
|
+
server2.broadcast({ type: "advisor_action", action: "rejected", decisionId, reason });
|
|
1270
|
+
json(res, 200, { ok: true, decisionId }, cors);
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (path === "/pending" && method === "GET") {
|
|
1274
|
+
const agentE = server2.getAgentE();
|
|
1275
|
+
const pending = agentE.log.query({ result: "skipped_override" });
|
|
1276
|
+
json(res, 200, {
|
|
1277
|
+
mode: agentE.getMode(),
|
|
1278
|
+
pending,
|
|
1279
|
+
count: pending.length
|
|
1280
|
+
}, cors);
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
221
1283
|
json(res, 404, { error: "Not found" }, cors);
|
|
222
1284
|
} catch (err) {
|
|
223
1285
|
console.error("[AgentE Server] Unhandled route error:", err);
|
|
@@ -234,8 +1296,10 @@ function send(ws, data) {
|
|
|
234
1296
|
ws.send(JSON.stringify(data));
|
|
235
1297
|
}
|
|
236
1298
|
}
|
|
1299
|
+
var MAX_WS_PAYLOAD = 1048576;
|
|
1300
|
+
var MAX_WS_CONNECTIONS = 100;
|
|
237
1301
|
function createWebSocketHandler(httpServer, server2) {
|
|
238
|
-
const wss = new import_ws.WebSocketServer({ server: httpServer });
|
|
1302
|
+
const wss = new import_ws.WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
|
|
239
1303
|
const aliveMap = /* @__PURE__ */ new WeakMap();
|
|
240
1304
|
const heartbeatInterval = setInterval(() => {
|
|
241
1305
|
for (const ws of wss.clients) {
|
|
@@ -250,6 +1314,10 @@ function createWebSocketHandler(httpServer, server2) {
|
|
|
250
1314
|
}
|
|
251
1315
|
}, 3e4);
|
|
252
1316
|
wss.on("connection", (ws) => {
|
|
1317
|
+
if (wss.clients.size > MAX_WS_CONNECTIONS) {
|
|
1318
|
+
ws.close(1013, "Server at capacity");
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
253
1321
|
console.log("[AgentE Server] Client connected");
|
|
254
1322
|
aliveMap.set(ws, true);
|
|
255
1323
|
ws.on("pong", () => {
|
|
@@ -355,9 +1423,20 @@ function createWebSocketHandler(httpServer, server2) {
|
|
|
355
1423
|
}
|
|
356
1424
|
});
|
|
357
1425
|
});
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
wss.
|
|
1426
|
+
function broadcast(data) {
|
|
1427
|
+
const payload = JSON.stringify(data);
|
|
1428
|
+
for (const ws of wss.clients) {
|
|
1429
|
+
if (ws.readyState === import_ws.WebSocket.OPEN) {
|
|
1430
|
+
ws.send(payload);
|
|
1431
|
+
}
|
|
1432
|
+
}
|
|
1433
|
+
}
|
|
1434
|
+
return {
|
|
1435
|
+
cleanup: () => {
|
|
1436
|
+
clearInterval(heartbeatInterval);
|
|
1437
|
+
wss.close();
|
|
1438
|
+
},
|
|
1439
|
+
broadcast
|
|
361
1440
|
};
|
|
362
1441
|
}
|
|
363
1442
|
|
|
@@ -368,11 +1447,12 @@ var AgentEServer = class {
|
|
|
368
1447
|
this.adjustmentQueue = [];
|
|
369
1448
|
this.alerts = [];
|
|
370
1449
|
this.startedAt = Date.now();
|
|
371
|
-
this.
|
|
1450
|
+
this.wsHandle = null;
|
|
372
1451
|
this.port = config.port ?? 3100;
|
|
373
1452
|
this.host = config.host ?? "0.0.0.0";
|
|
374
1453
|
this.validateState = config.validateState ?? true;
|
|
375
|
-
this.corsOrigin = config.corsOrigin ?? "
|
|
1454
|
+
this.corsOrigin = config.corsOrigin ?? "http://localhost:3100";
|
|
1455
|
+
this.serveDashboard = config.serveDashboard ?? true;
|
|
376
1456
|
const adapter = {
|
|
377
1457
|
getState: () => {
|
|
378
1458
|
if (!this.lastState) {
|
|
@@ -421,7 +1501,7 @@ var AgentEServer = class {
|
|
|
421
1501
|
this.server = http.createServer(routeHandler);
|
|
422
1502
|
}
|
|
423
1503
|
async start() {
|
|
424
|
-
this.
|
|
1504
|
+
this.wsHandle = createWebSocketHandler(this.server, this);
|
|
425
1505
|
return new Promise((resolve) => {
|
|
426
1506
|
this.server.listen(this.port, this.host, () => {
|
|
427
1507
|
const addr = this.getAddress();
|
|
@@ -432,7 +1512,7 @@ var AgentEServer = class {
|
|
|
432
1512
|
}
|
|
433
1513
|
async stop() {
|
|
434
1514
|
this.agentE.stop();
|
|
435
|
-
if (this.
|
|
1515
|
+
if (this.wsHandle) this.wsHandle.cleanup();
|
|
436
1516
|
return new Promise((resolve, reject) => {
|
|
437
1517
|
this.server.close((err) => {
|
|
438
1518
|
if (err) reject(err);
|
|
@@ -526,6 +1606,9 @@ var AgentEServer = class {
|
|
|
526
1606
|
constrain(param, bounds) {
|
|
527
1607
|
this.agentE.constrain(param, bounds);
|
|
528
1608
|
}
|
|
1609
|
+
broadcast(data) {
|
|
1610
|
+
if (this.wsHandle) this.wsHandle.broadcast(data);
|
|
1611
|
+
}
|
|
529
1612
|
};
|
|
530
1613
|
|
|
531
1614
|
// src/cli.ts
|