@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/dist/index.js CHANGED
@@ -30,8 +30,955 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
30
30
  ));
31
31
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
32
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;'); }
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
+ var init_dashboard = __esm({
969
+ "src/dashboard.ts"() {
970
+ "use strict";
971
+ }
972
+ });
973
+
33
974
  // src/routes.ts
975
+ function setSecurityHeaders(res) {
976
+ res.setHeader("X-Content-Type-Options", "nosniff");
977
+ res.setHeader("X-Frame-Options", "DENY");
978
+ res.setHeader("Referrer-Policy", "strict-origin-when-cross-origin");
979
+ }
34
980
  function setCorsHeaders(res, origin) {
981
+ setSecurityHeaders(res);
35
982
  res.setHeader("Access-Control-Allow-Origin", origin);
36
983
  res.setHeader("Access-Control-Allow-Methods", "GET, POST, OPTIONS");
37
984
  res.setHeader("Access-Control-Allow-Headers", "Content-Type");
@@ -87,21 +1034,19 @@ function createRouteHandler(server) {
87
1034
  const payload = parsed;
88
1035
  const state = payload["state"] ?? parsed;
89
1036
  const events = payload["events"];
90
- if (server.validateState) {
91
- const validation = (0, import_core.validateEconomyState)(state);
92
- if (!validation.valid) {
93
- json(res, 400, {
94
- error: "invalid_state",
95
- validationErrors: validation.errors
96
- }, cors);
97
- return;
98
- }
1037
+ const validation = server.validateState ? (0, import_core.validateEconomyState)(state) : null;
1038
+ if (validation && !validation.valid) {
1039
+ json(res, 400, {
1040
+ error: "invalid_state",
1041
+ validationErrors: validation.errors
1042
+ }, cors);
1043
+ return;
99
1044
  }
100
1045
  const result = await server.processTick(
101
1046
  state,
102
1047
  Array.isArray(events) ? events : void 0
103
1048
  );
104
- const warnings = server.validateState ? (0, import_core.validateEconomyState)(state).warnings : [];
1049
+ const warnings = validation?.warnings ?? [];
105
1050
  json(res, 200, {
106
1051
  adjustments: result.adjustments,
107
1052
  alerts: result.alerts.map((a) => ({
@@ -129,12 +1074,18 @@ function createRouteHandler(server) {
129
1074
  return;
130
1075
  }
131
1076
  if (path === "/decisions" && method === "GET") {
132
- const limit = parseInt(url.searchParams.get("limit") ?? "100", 10);
133
- const since = url.searchParams.get("since");
1077
+ const rawLimit = parseInt(url.searchParams.get("limit") ?? "100", 10);
1078
+ const limit = Math.min(Math.max(isNaN(rawLimit) ? 100 : rawLimit, 1), 1e3);
1079
+ const sinceParam = url.searchParams.get("since");
134
1080
  const agentE = server.getAgentE();
135
1081
  let decisions;
136
- if (since) {
137
- decisions = agentE.getDecisions({ since: parseInt(since, 10) });
1082
+ if (sinceParam) {
1083
+ const since = parseInt(sinceParam, 10);
1084
+ if (isNaN(since)) {
1085
+ json(res, 400, { error: 'Invalid "since" parameter \u2014 must be a number' }, cors);
1086
+ return;
1087
+ }
1088
+ decisions = agentE.getDecisions({ since });
138
1089
  } else {
139
1090
  decisions = agentE.log.latest(limit);
140
1091
  }
@@ -165,6 +1116,14 @@ function createRouteHandler(server) {
165
1116
  for (const c of config["constrain"]) {
166
1117
  if (c && typeof c === "object" && typeof c["param"] === "string" && typeof c["min"] === "number" && typeof c["max"] === "number") {
167
1118
  const constraint = c;
1119
+ if (!isFinite(constraint.min) || !isFinite(constraint.max)) {
1120
+ json(res, 400, { error: "Constraint bounds must be finite numbers" }, cors);
1121
+ return;
1122
+ }
1123
+ if (constraint.min > constraint.max) {
1124
+ json(res, 400, { error: "Constraint min cannot exceed max" }, cors);
1125
+ return;
1126
+ }
168
1127
  server.constrain(constraint.param, { min: constraint.min, max: constraint.max });
169
1128
  }
170
1129
  }
@@ -219,6 +1178,112 @@ function createRouteHandler(server) {
219
1178
  }, cors);
220
1179
  return;
221
1180
  }
1181
+ if (path === "/" && method === "GET" && server.serveDashboard) {
1182
+ setCorsHeaders(res, cors);
1183
+ 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:");
1184
+ res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
1185
+ res.end(getDashboardHtml());
1186
+ return;
1187
+ }
1188
+ if (path === "/metrics" && method === "GET") {
1189
+ const agentE = server.getAgentE();
1190
+ const latest = agentE.store.latest();
1191
+ const history = agentE.store.recentHistory(100);
1192
+ json(res, 200, { latest, history }, cors);
1193
+ return;
1194
+ }
1195
+ if (path === "/metrics/personas" && method === "GET") {
1196
+ const agentE = server.getAgentE();
1197
+ const latest = agentE.store.latest();
1198
+ const dist = latest.personaDistribution || {};
1199
+ const total = Object.values(dist).reduce((s, v) => s + v, 0);
1200
+ json(res, 200, { distribution: dist, total }, cors);
1201
+ return;
1202
+ }
1203
+ if (path === "/approve" && method === "POST") {
1204
+ const body = await readBody(req);
1205
+ let parsed;
1206
+ try {
1207
+ parsed = JSON.parse(body);
1208
+ } catch {
1209
+ json(res, 400, { error: "Invalid JSON" }, cors);
1210
+ return;
1211
+ }
1212
+ const payload = parsed;
1213
+ const decisionId = payload["decisionId"];
1214
+ if (!decisionId) {
1215
+ json(res, 400, { error: "missing_decision_id" }, cors);
1216
+ return;
1217
+ }
1218
+ const agentE = server.getAgentE();
1219
+ if (agentE.getMode() !== "advisor") {
1220
+ json(res, 400, { error: "not_in_advisor_mode" }, cors);
1221
+ return;
1222
+ }
1223
+ const entry = agentE.log.getById(decisionId);
1224
+ if (!entry) {
1225
+ json(res, 404, { error: "decision_not_found" }, cors);
1226
+ return;
1227
+ }
1228
+ if (entry.result !== "skipped_override") {
1229
+ json(res, 409, { error: "decision_not_pending", currentResult: entry.result }, cors);
1230
+ return;
1231
+ }
1232
+ await agentE.apply(entry.plan);
1233
+ agentE.log.updateResult(decisionId, "applied");
1234
+ server.broadcast({ type: "advisor_action", action: "approved", decisionId });
1235
+ json(res, 200, {
1236
+ ok: true,
1237
+ parameter: entry.plan.parameter,
1238
+ value: entry.plan.targetValue
1239
+ }, cors);
1240
+ return;
1241
+ }
1242
+ if (path === "/reject" && method === "POST") {
1243
+ const body = await readBody(req);
1244
+ let parsed;
1245
+ try {
1246
+ parsed = JSON.parse(body);
1247
+ } catch {
1248
+ json(res, 400, { error: "Invalid JSON" }, cors);
1249
+ return;
1250
+ }
1251
+ const payload = parsed;
1252
+ const decisionId = payload["decisionId"];
1253
+ const reason = payload["reason"] || void 0;
1254
+ if (!decisionId) {
1255
+ json(res, 400, { error: "missing_decision_id" }, cors);
1256
+ return;
1257
+ }
1258
+ const agentE = server.getAgentE();
1259
+ if (agentE.getMode() !== "advisor") {
1260
+ json(res, 400, { error: "not_in_advisor_mode" }, cors);
1261
+ return;
1262
+ }
1263
+ const entry = agentE.log.getById(decisionId);
1264
+ if (!entry) {
1265
+ json(res, 404, { error: "decision_not_found" }, cors);
1266
+ return;
1267
+ }
1268
+ if (entry.result !== "skipped_override") {
1269
+ json(res, 409, { error: "decision_not_pending", currentResult: entry.result }, cors);
1270
+ return;
1271
+ }
1272
+ agentE.log.updateResult(decisionId, "rejected", reason);
1273
+ server.broadcast({ type: "advisor_action", action: "rejected", decisionId, reason });
1274
+ json(res, 200, { ok: true, decisionId }, cors);
1275
+ return;
1276
+ }
1277
+ if (path === "/pending" && method === "GET") {
1278
+ const agentE = server.getAgentE();
1279
+ const pending = agentE.log.query({ result: "skipped_override" });
1280
+ json(res, 200, {
1281
+ mode: agentE.getMode(),
1282
+ pending,
1283
+ count: pending.length
1284
+ }, cors);
1285
+ return;
1286
+ }
222
1287
  json(res, 404, { error: "Not found" }, cors);
223
1288
  } catch (err) {
224
1289
  console.error("[AgentE Server] Unhandled route error:", err);
@@ -231,6 +1296,7 @@ var init_routes = __esm({
231
1296
  "src/routes.ts"() {
232
1297
  "use strict";
233
1298
  import_core = require("@agent-e/core");
1299
+ init_dashboard();
234
1300
  MAX_BODY_BYTES = 1048576;
235
1301
  }
236
1302
  });
@@ -242,7 +1308,7 @@ function send(ws, data) {
242
1308
  }
243
1309
  }
244
1310
  function createWebSocketHandler(httpServer, server) {
245
- const wss = new import_ws.WebSocketServer({ server: httpServer });
1311
+ const wss = new import_ws.WebSocketServer({ server: httpServer, maxPayload: MAX_WS_PAYLOAD });
246
1312
  const aliveMap = /* @__PURE__ */ new WeakMap();
247
1313
  const heartbeatInterval = setInterval(() => {
248
1314
  for (const ws of wss.clients) {
@@ -257,6 +1323,10 @@ function createWebSocketHandler(httpServer, server) {
257
1323
  }
258
1324
  }, 3e4);
259
1325
  wss.on("connection", (ws) => {
1326
+ if (wss.clients.size > MAX_WS_CONNECTIONS) {
1327
+ ws.close(1013, "Server at capacity");
1328
+ return;
1329
+ }
260
1330
  console.log("[AgentE Server] Client connected");
261
1331
  aliveMap.set(ws, true);
262
1332
  ws.on("pong", () => {
@@ -362,17 +1432,30 @@ function createWebSocketHandler(httpServer, server) {
362
1432
  }
363
1433
  });
364
1434
  });
365
- return () => {
366
- clearInterval(heartbeatInterval);
367
- wss.close();
1435
+ function broadcast(data) {
1436
+ const payload = JSON.stringify(data);
1437
+ for (const ws of wss.clients) {
1438
+ if (ws.readyState === import_ws.WebSocket.OPEN) {
1439
+ ws.send(payload);
1440
+ }
1441
+ }
1442
+ }
1443
+ return {
1444
+ cleanup: () => {
1445
+ clearInterval(heartbeatInterval);
1446
+ wss.close();
1447
+ },
1448
+ broadcast
368
1449
  };
369
1450
  }
370
- var import_ws, import_core2;
1451
+ var import_ws, import_core2, MAX_WS_PAYLOAD, MAX_WS_CONNECTIONS;
371
1452
  var init_websocket = __esm({
372
1453
  "src/websocket.ts"() {
373
1454
  "use strict";
374
1455
  import_ws = require("ws");
375
1456
  import_core2 = require("@agent-e/core");
1457
+ MAX_WS_PAYLOAD = 1048576;
1458
+ MAX_WS_CONNECTIONS = 100;
376
1459
  }
377
1460
  });
378
1461
 
@@ -395,11 +1478,12 @@ var init_AgentEServer = __esm({
395
1478
  this.adjustmentQueue = [];
396
1479
  this.alerts = [];
397
1480
  this.startedAt = Date.now();
398
- this.cleanupWs = null;
1481
+ this.wsHandle = null;
399
1482
  this.port = config.port ?? 3100;
400
1483
  this.host = config.host ?? "0.0.0.0";
401
1484
  this.validateState = config.validateState ?? true;
402
- this.corsOrigin = config.corsOrigin ?? "*";
1485
+ this.corsOrigin = config.corsOrigin ?? "http://localhost:3100";
1486
+ this.serveDashboard = config.serveDashboard ?? true;
403
1487
  const adapter = {
404
1488
  getState: () => {
405
1489
  if (!this.lastState) {
@@ -448,7 +1532,7 @@ var init_AgentEServer = __esm({
448
1532
  this.server = http.createServer(routeHandler);
449
1533
  }
450
1534
  async start() {
451
- this.cleanupWs = createWebSocketHandler(this.server, this);
1535
+ this.wsHandle = createWebSocketHandler(this.server, this);
452
1536
  return new Promise((resolve) => {
453
1537
  this.server.listen(this.port, this.host, () => {
454
1538
  const addr = this.getAddress();
@@ -459,7 +1543,7 @@ var init_AgentEServer = __esm({
459
1543
  }
460
1544
  async stop() {
461
1545
  this.agentE.stop();
462
- if (this.cleanupWs) this.cleanupWs();
1546
+ if (this.wsHandle) this.wsHandle.cleanup();
463
1547
  return new Promise((resolve, reject) => {
464
1548
  this.server.close((err) => {
465
1549
  if (err) reject(err);
@@ -553,6 +1637,9 @@ var init_AgentEServer = __esm({
553
1637
  constrain(param, bounds) {
554
1638
  this.agentE.constrain(param, bounds);
555
1639
  }
1640
+ broadcast(data) {
1641
+ if (this.wsHandle) this.wsHandle.broadcast(data);
1642
+ }
556
1643
  };
557
1644
  }
558
1645
  });