@agent-e/server 1.7.1 → 1.7.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -22,96 +22,109 @@ function getDashboardHtml() {
22
22
  <title>AgentE Dashboard</title>
23
23
  <link rel="preconnect" href="https://fonts.googleapis.com">
24
24
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
25
- <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">
25
+ <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,wght@14..32,400;14..32,500;14..32,600;14..32,700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
26
26
  <script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
27
27
  <style>
28
+ *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
29
+
28
30
  :root {
29
31
  --bg-root: #09090b;
30
32
  --bg-panel: #18181b;
31
- --bg-panel-hover: #1f1f23;
33
+ --bg-terminal: #09090b;
32
34
  --border: #27272a;
33
- --border-light: #3f3f46;
34
- --text-primary: #f4f4f5;
35
- --text-secondary: #a1a1aa;
36
- --text-muted: #71717a;
37
- --text-dim: #52525b;
35
+ --text-primary: #ffffff;
36
+ --text-secondary: #52525b;
37
+ --text-tertiary: #a1a1aa;
38
+ --text-value: #d4d4d8;
38
39
  --accent: #22c55e;
39
- --accent-dim: #166534;
40
40
  --warning: #eab308;
41
- --warning-dim: #854d0e;
42
41
  --danger: #ef4444;
43
- --danger-dim: #991b1b;
44
- --blue: #3b82f6;
45
- --font-sans: 'IBM Plex Sans', system-ui, sans-serif;
46
- --font-mono: 'JetBrains Mono', monospace;
42
+ --info: #71717a;
47
43
  }
48
44
 
49
- * { margin: 0; padding: 0; box-sizing: border-box; }
45
+ html { scroll-behavior: smooth; }
50
46
 
51
47
  body {
52
48
  background: var(--bg-root);
53
49
  color: var(--text-primary);
54
- font-family: var(--font-sans);
55
- font-size: 14px;
50
+ font-family: 'Inter', system-ui, sans-serif;
51
+ -webkit-font-smoothing: antialiased;
56
52
  line-height: 1.5;
57
- overflow-x: hidden;
53
+ min-height: 100dvh;
54
+ overflow-y: auto;
58
55
  }
59
56
 
60
- /* \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 */
57
+ /* -- Header -- */
61
58
  .header {
62
59
  position: sticky;
63
60
  top: 0;
64
- z-index: 100;
65
- background: var(--bg-root);
61
+ z-index: 50;
62
+ background: var(--bg-panel);
66
63
  border-bottom: 1px solid var(--border);
67
64
  padding: 12px 24px;
68
65
  display: flex;
69
66
  align-items: center;
70
- gap: 24px;
71
- backdrop-filter: blur(8px);
67
+ justify-content: space-between;
68
+ gap: 16px;
72
69
  }
73
70
 
74
- .header-brand {
75
- font-weight: 600;
76
- font-size: 16px;
71
+ .header-left {
72
+ display: flex;
73
+ align-items: center;
74
+ gap: 10px;
75
+ }
76
+
77
+ .header-logo {
78
+ font-family: 'JetBrains Mono', monospace;
79
+ font-size: 15px;
80
+ font-weight: 500;
77
81
  color: var(--text-primary);
78
- white-space: nowrap;
79
82
  }
80
83
 
81
- .header-brand span { color: var(--accent); }
84
+ .header-version {
85
+ font-family: 'JetBrains Mono', monospace;
86
+ font-size: 11px;
87
+ color: var(--text-secondary);
88
+ background: var(--bg-root);
89
+ padding: 2px 8px;
90
+ border-radius: 4px;
91
+ }
82
92
 
83
- .kpi-row {
93
+ .header-right {
84
94
  display: flex;
85
- gap: 20px;
86
- flex-wrap: wrap;
87
95
  align-items: center;
88
- margin-left: auto;
96
+ gap: 20px;
89
97
  }
90
98
 
91
- .kpi {
99
+ .kpi-pill {
92
100
  display: flex;
93
101
  align-items: center;
94
102
  gap: 6px;
95
- font-size: 13px;
103
+ font-size: 12px;
104
+ font-family: 'JetBrains Mono', monospace;
105
+ }
106
+
107
+ .kpi-pill .label {
96
108
  color: var(--text-secondary);
109
+ text-transform: uppercase;
110
+ letter-spacing: 0.05em;
97
111
  }
98
112
 
99
- .kpi-value {
100
- font-family: var(--font-mono);
101
- font-weight: 500;
102
- color: var(--text-primary);
103
- font-size: 13px;
113
+ .kpi-pill .value {
114
+ color: var(--text-value);
115
+ font-variant-numeric: tabular-nums;
104
116
  }
105
117
 
106
- .kpi-value.health-good { color: var(--accent); }
107
- .kpi-value.health-warn { color: var(--warning); }
108
- .kpi-value.health-bad { color: var(--danger); }
118
+ .kpi-pill .value.health-good { color: var(--accent); }
119
+ .kpi-pill .value.health-warn { color: var(--warning); }
120
+ .kpi-pill .value.health-bad { color: var(--danger); }
109
121
 
110
122
  .live-dot {
111
- width: 8px;
112
- height: 8px;
123
+ width: 6px;
124
+ height: 6px;
113
125
  border-radius: 50%;
114
126
  background: var(--accent);
127
+ flex-shrink: 0;
115
128
  animation: pulse 2s ease-in-out infinite;
116
129
  }
117
130
 
@@ -125,82 +138,159 @@ function getDashboardHtml() {
125
138
  50% { opacity: 0.4; }
126
139
  }
127
140
 
128
- /* \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 */
129
- .container {
141
+ /* -- Advisor Banner -- */
142
+ .advisor-banner {
143
+ display: none;
144
+ background: rgba(234,179,8,0.08);
145
+ border-bottom: 1px solid rgba(234,179,8,0.2);
146
+ padding: 8px 24px;
147
+ font-family: 'JetBrains Mono', monospace;
148
+ font-size: 11px;
149
+ color: var(--warning);
150
+ text-align: center;
151
+ letter-spacing: 0.03em;
152
+ }
153
+
154
+ .advisor-mode .advisor-banner { display: block; }
155
+
156
+ /* -- Advisor-specific: pending pill -- */
157
+ .pending-pill {
158
+ display: none;
159
+ background: rgba(234,179,8,0.15);
160
+ border-radius: 4px;
161
+ padding: 2px 8px;
162
+ cursor: pointer;
163
+ }
164
+
165
+ .pending-pill:hover { background: rgba(234,179,8,0.25); }
166
+
167
+ .advisor-mode .pending-pill { display: flex; }
168
+
169
+ /* -- Mode value color switching -- */
170
+ .mode-value-auto { color: var(--accent); }
171
+ .mode-value-advisor { color: var(--warning); }
172
+
173
+ /* -- Layout -- */
174
+ .dashboard {
130
175
  max-width: 1440px;
131
176
  margin: 0 auto;
132
- padding: 20px 24px;
177
+ padding: 16px;
133
178
  display: flex;
134
179
  flex-direction: column;
135
- gap: 16px;
180
+ gap: 12px;
136
181
  }
137
182
 
183
+ /* -- Panels -- */
138
184
  .panel {
139
185
  background: var(--bg-panel);
140
186
  border: 1px solid var(--border);
141
187
  border-radius: 8px;
142
- padding: 16px;
188
+ padding: 16px 20px;
189
+ min-width: 0;
190
+ display: flex;
191
+ flex-direction: column;
192
+ }
193
+
194
+ .panel-header {
195
+ display: flex;
196
+ align-items: center;
197
+ justify-content: space-between;
198
+ margin-bottom: 12px;
199
+ flex-shrink: 0;
143
200
  }
144
201
 
145
202
  .panel-title {
146
- font-size: 13px;
203
+ font-size: 11px;
147
204
  font-weight: 600;
148
- color: var(--text-secondary);
149
205
  text-transform: uppercase;
150
- letter-spacing: 0.05em;
151
- margin-bottom: 12px;
206
+ letter-spacing: 0.08em;
207
+ color: var(--info);
208
+ }
209
+
210
+ .panel-meta {
211
+ font-size: 11px;
212
+ font-family: 'JetBrains Mono', monospace;
213
+ color: var(--text-secondary);
214
+ display: flex;
215
+ align-items: center;
216
+ gap: 8px;
217
+ }
218
+
219
+ .panel-meta .live-label {
220
+ color: var(--accent);
221
+ display: flex;
222
+ align-items: center;
223
+ gap: 4px;
224
+ }
225
+
226
+ .panel-body {
227
+ flex: 1;
228
+ min-height: 0;
229
+ overflow: hidden;
152
230
  }
153
231
 
154
- /* \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 */
155
- .charts-grid {
232
+ /* -- Health Charts -- */
233
+ .charts-panel .chart-row {
156
234
  display: grid;
157
- grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
235
+ grid-template-columns: repeat(4, 1fr);
158
236
  gap: 16px;
159
237
  }
160
238
 
161
- .chart-box {
162
- background: var(--bg-panel);
163
- border: 1px solid var(--border);
164
- border-radius: 8px;
165
- padding: 16px;
239
+ .mini-chart { position: relative; }
240
+ .mini-chart-label {
241
+ font-size: 10px;
242
+ font-weight: 600;
243
+ text-transform: uppercase;
244
+ letter-spacing: 0.06em;
245
+ color: var(--text-secondary);
246
+ margin-bottom: 4px;
166
247
  }
248
+ .mini-chart canvas { width: 100% !important; height: 64px !important; }
167
249
 
168
- .chart-box canvas { width: 100% !important; height: 160px !important; }
250
+ @media (max-width: 900px) {
251
+ .charts-panel .chart-row { grid-template-columns: 1fr 1fr; }
252
+ }
169
253
 
170
- .chart-label {
171
- font-size: 12px;
172
- color: var(--text-muted);
173
- font-weight: 500;
174
- margin-bottom: 8px;
254
+ @media (max-width: 500px) {
255
+ .charts-panel .chart-row { grid-template-columns: 1fr; }
175
256
  }
176
257
 
177
- .chart-value {
178
- font-family: var(--font-mono);
179
- font-size: 22px;
180
- font-weight: 500;
181
- color: var(--text-primary);
182
- margin-bottom: 8px;
258
+ /* -- Terminal Feed -- */
259
+ .terminal-panel {
260
+ border-left: 2px solid var(--accent);
261
+ height: 380px;
262
+ overflow: hidden;
263
+ }
264
+
265
+ .advisor-mode .terminal-panel {
266
+ border-left-color: var(--warning);
183
267
  }
184
268
 
185
- /* \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 */
186
269
  .terminal {
187
- background: var(--bg-root);
188
- border: 1px solid var(--border);
189
- border-radius: 8px;
190
- height: 380px;
270
+ background: var(--bg-terminal);
271
+ border-radius: 6px;
272
+ padding: 12px 16px;
273
+ height: 100%;
191
274
  overflow-y: auto;
192
- font-family: var(--font-mono);
193
- font-size: 12px;
275
+ font-family: 'JetBrains Mono', monospace;
276
+ font-size: 12.5px;
194
277
  line-height: 1.7;
195
- padding: 12px 16px;
278
+ display: flex;
279
+ flex-direction: column;
280
+ }
281
+
282
+ .terminal-inner {
283
+ margin-top: auto;
196
284
  }
197
285
 
198
- .terminal::-webkit-scrollbar { width: 6px; }
286
+ .terminal::-webkit-scrollbar { width: 4px; }
199
287
  .terminal::-webkit-scrollbar-track { background: transparent; }
200
- .terminal::-webkit-scrollbar-thumb { background: var(--border-light); border-radius: 3px; }
288
+ .terminal::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
201
289
 
202
290
  .term-line {
203
291
  white-space: nowrap;
292
+ overflow: hidden;
293
+ text-overflow: ellipsis;
204
294
  opacity: 0;
205
295
  transform: translateY(4px);
206
296
  animation: termIn 0.3s ease-out forwards;
@@ -210,120 +300,197 @@ function getDashboardHtml() {
210
300
  to { opacity: 1; transform: translateY(0); }
211
301
  }
212
302
 
213
- .t-tick { color: var(--text-dim); }
303
+ .t-tick { color: var(--text-secondary); }
214
304
  .t-ok { color: var(--accent); }
305
+ .t-check { color: var(--accent); }
215
306
  .t-skip { color: var(--warning); }
216
307
  .t-fail { color: var(--danger); }
217
308
  .t-principle { color: var(--text-primary); font-weight: 500; }
218
- .t-param { color: var(--text-secondary); }
219
- .t-old { color: #d4d4d8; font-variant-numeric: tabular-nums; }
220
- .t-arrow { color: var(--text-dim); }
309
+ .t-param { color: var(--text-tertiary); }
310
+ .t-old { color: var(--text-value); font-variant-numeric: tabular-nums; }
311
+ .t-arrow { color: var(--info); }
221
312
  .t-new { color: var(--accent); font-variant-numeric: tabular-nums; }
222
- .t-meta { color: var(--text-dim); }
313
+ .t-meta { color: var(--text-secondary); }
314
+ .t-violation-id { color: var(--warning); }
315
+ .t-violation-desc { color: var(--text-tertiary); }
316
+ .t-status-label { color: var(--text-tertiary); }
317
+ .t-status-value { color: var(--accent); font-variant-numeric: tabular-nums; }
318
+ .t-dim { color: var(--text-secondary); }
319
+ .t-white { color: var(--text-primary); }
320
+ .t-separator { color: var(--info); }
321
+ .t-pending-icon { color: var(--warning); }
322
+ .t-pending-val { color: var(--warning); font-variant-numeric: tabular-nums; }
323
+
324
+ /* -- Advisor Inline Buttons -- */
325
+ .advisor-btn {
326
+ display: none;
327
+ font-family: 'JetBrains Mono', monospace;
328
+ font-size: 10px;
329
+ border-radius: 3px;
330
+ padding: 2px 8px;
331
+ cursor: pointer;
332
+ border: 1px solid;
333
+ margin-left: 6px;
334
+ vertical-align: middle;
335
+ line-height: 1.4;
336
+ transition: background 0.15s;
337
+ }
223
338
 
224
- /* \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 */
225
- .alerts-container {
226
- display: flex;
227
- flex-direction: column;
228
- gap: 8px;
229
- max-height: 320px;
230
- overflow-y: auto;
339
+ .advisor-mode .advisor-btn { display: inline-flex; align-items: center; }
340
+
341
+ .advisor-btn.approve-btn {
342
+ background: rgba(34,197,94,0.15);
343
+ color: var(--accent);
344
+ border-color: rgba(34,197,94,0.3);
231
345
  }
346
+ .advisor-btn.approve-btn:hover { background: rgba(34,197,94,0.25); }
232
347
 
233
- .alert-card {
234
- display: flex;
235
- align-items: flex-start;
236
- gap: 12px;
237
- padding: 12px;
238
- border-radius: 6px;
239
- border: 1px solid var(--border);
240
- background: var(--bg-panel);
241
- transition: opacity 0.3s, transform 0.3s;
348
+ .advisor-btn.reject-btn {
349
+ background: rgba(239,68,68,0.1);
350
+ color: var(--danger);
351
+ border-color: rgba(239,68,68,0.2);
242
352
  }
353
+ .advisor-btn.reject-btn:hover { background: rgba(239,68,68,0.2); }
243
354
 
244
- .alert-card.fade-out {
245
- opacity: 0;
246
- transform: translateX(20px);
355
+ /* Approved/Rejected flash labels */
356
+ .action-flash {
357
+ font-family: 'JetBrains Mono', monospace;
358
+ font-size: 10px;
359
+ margin-left: 8px;
360
+ animation: flashIn 0.3s ease-out;
247
361
  }
248
362
 
249
- .alert-severity {
250
- font-family: var(--font-mono);
251
- font-weight: 600;
252
- font-size: 13px;
253
- padding: 2px 8px;
254
- border-radius: 4px;
255
- white-space: nowrap;
363
+ .action-flash.approved { color: var(--accent); }
364
+ .action-flash.rejected { color: var(--info); }
365
+
366
+ @keyframes flashIn {
367
+ from { opacity: 0; transform: translateX(-4px); }
368
+ to { opacity: 1; transform: translateX(0); }
256
369
  }
257
370
 
258
- .sev-high { background: var(--danger-dim); color: var(--danger); }
259
- .sev-med { background: var(--warning-dim); color: var(--warning); }
260
- .sev-low { background: var(--accent-dim); color: var(--accent); }
371
+ /* Dimmed line after rejection */
372
+ .term-line.rejected-line { opacity: 0.5; }
261
373
 
262
- .alert-body { flex: 1; }
263
- .alert-principle { font-weight: 500; font-size: 13px; }
264
- .alert-reason { color: var(--text-secondary); font-size: 12px; margin-top: 2px; }
374
+ /* -- Alerts -- */
375
+ .alert-card {
376
+ background: var(--bg-root);
377
+ border-radius: 6px;
378
+ padding: 10px 14px;
379
+ margin-bottom: 8px;
380
+ border-left: 3px solid transparent;
381
+ overflow: hidden;
382
+ }
265
383
 
266
- /* \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 */
267
- .violations-table {
268
- width: 100%;
269
- border-collapse: collapse;
270
- font-size: 12px;
384
+ .alert-card.sev-high { border-left-color: var(--danger); }
385
+ .alert-card.sev-med { border-left-color: var(--warning); }
386
+ .alert-card.sev-low { border-left-color: var(--accent); }
387
+
388
+ .alert-top {
389
+ display: flex;
390
+ align-items: center;
391
+ gap: 8px;
392
+ margin-bottom: 4px;
271
393
  }
272
394
 
273
- .violations-table th {
274
- text-align: left;
275
- color: var(--text-muted);
276
- font-weight: 500;
277
- padding: 6px 10px;
278
- border-bottom: 1px solid var(--border);
395
+ .sev-badge {
396
+ width: 20px;
397
+ height: 20px;
398
+ border-radius: 50%;
399
+ display: flex;
400
+ align-items: center;
401
+ justify-content: center;
402
+ font-size: 10px;
403
+ font-weight: 700;
404
+ font-family: 'JetBrains Mono', monospace;
405
+ color: var(--bg-root);
406
+ flex-shrink: 0;
407
+ }
408
+
409
+ .sev-badge.high { background: var(--danger); }
410
+ .sev-badge.med { background: var(--warning); }
411
+ .sev-badge.low { background: var(--accent); }
412
+
413
+ .alert-principle-id { color: var(--warning); font-family: 'JetBrains Mono', monospace; font-size: 11px; }
414
+ .alert-principle-name { color: var(--text-primary); font-size: 12px; font-weight: 500; }
415
+ .alert-evidence { color: var(--text-tertiary); font-size: 10px; margin-top: 2px; }
416
+ .alert-suggestion { color: var(--accent); font-size: 10px; margin-top: 2px; }
417
+
418
+ .alert-approve-btn {
419
+ display: none;
420
+ font-family: 'JetBrains Mono', monospace;
421
+ font-size: 10px;
422
+ background: rgba(34,197,94,0.15);
423
+ color: var(--accent);
424
+ border: 1px solid rgba(34,197,94,0.3);
425
+ border-radius: 3px;
426
+ padding: 3px 10px;
279
427
  cursor: pointer;
280
- user-select: none;
428
+ margin-top: 8px;
429
+ transition: background 0.15s;
281
430
  }
282
431
 
283
- .violations-table th:hover { color: var(--text-secondary); }
432
+ .alert-approve-btn:hover { background: rgba(34,197,94,0.25); }
284
433
 
285
- .violations-table td {
286
- padding: 6px 10px;
287
- border-bottom: 1px solid var(--border);
288
- color: var(--text-secondary);
289
- font-family: var(--font-mono);
290
- font-size: 11px;
434
+ .alert-reject-btn {
435
+ display: none;
436
+ font-family: 'JetBrains Mono', monospace;
437
+ font-size: 10px;
438
+ background: rgba(239,68,68,0.1);
439
+ color: var(--danger);
440
+ border: 1px solid rgba(239,68,68,0.2);
441
+ border-radius: 3px;
442
+ padding: 3px 10px;
443
+ cursor: pointer;
444
+ margin-top: 8px;
445
+ margin-left: 6px;
446
+ transition: background 0.15s;
291
447
  }
292
448
 
293
- .violations-table tr:hover td { background: var(--bg-panel-hover); }
449
+ .alert-reject-btn:hover { background: rgba(239,68,68,0.2); }
294
450
 
295
- /* \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 */
296
- .split-row {
297
- display: grid;
298
- grid-template-columns: 1fr 1fr;
299
- gap: 16px;
451
+ .advisor-mode .alert-approve-btn { display: inline-block; }
452
+ .advisor-mode .alert-reject-btn { display: inline-block; }
453
+
454
+ /* Alert resolved state */
455
+ .alert-card.resolved {
456
+ opacity: 0;
457
+ max-height: 0;
458
+ padding: 0 14px;
459
+ margin-bottom: 0;
460
+ overflow: hidden;
461
+ transition: opacity 0.4s ease-out, max-height 0.5s ease-out 0.1s, padding 0.5s ease-out 0.1s, margin 0.5s ease-out 0.1s;
300
462
  }
301
463
 
302
- @media (max-width: 800px) {
303
- .split-row { grid-template-columns: 1fr; }
464
+ .alerts-scroll {
465
+ overflow-y: auto;
466
+ max-height: 300px;
304
467
  }
305
468
 
306
- /* \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 */
307
- .persona-bars { display: flex; flex-direction: column; gap: 6px; }
469
+ .alerts-scroll::-webkit-scrollbar { width: 4px; }
470
+ .alerts-scroll::-webkit-scrollbar-track { background: transparent; }
471
+ .alerts-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
308
472
 
473
+ /* -- Persona Bars -- */
309
474
  .persona-row {
310
475
  display: flex;
311
476
  align-items: center;
312
477
  gap: 8px;
313
- font-size: 12px;
478
+ margin-bottom: 5px;
479
+ font-size: 11px;
480
+ font-family: 'JetBrains Mono', monospace;
314
481
  }
315
482
 
316
483
  .persona-label {
317
484
  width: 100px;
318
485
  text-align: right;
319
- color: var(--text-secondary);
320
- font-size: 11px;
486
+ color: var(--text-tertiary);
321
487
  flex-shrink: 0;
488
+ font-variant-numeric: tabular-nums;
322
489
  }
323
490
 
324
491
  .persona-bar-track {
325
492
  flex: 1;
326
- height: 16px;
493
+ height: 12px;
327
494
  background: var(--bg-root);
328
495
  border-radius: 3px;
329
496
  overflow: hidden;
@@ -331,166 +498,311 @@ function getDashboardHtml() {
331
498
 
332
499
  .persona-bar-fill {
333
500
  height: 100%;
334
- background: var(--accent);
335
501
  border-radius: 3px;
336
- transition: width 0.5s ease;
502
+ transition: width 0.6s ease-out;
337
503
  }
338
504
 
339
505
  .persona-pct {
340
- width: 40px;
341
- font-family: var(--font-mono);
342
- font-size: 11px;
343
- color: var(--text-muted);
506
+ width: 32px;
507
+ text-align: right;
508
+ color: var(--text-secondary);
509
+ font-variant-numeric: tabular-nums;
344
510
  }
345
511
 
346
- /* \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 */
347
- .registry-list { display: flex; flex-direction: column; gap: 4px; }
348
-
349
- .registry-item {
512
+ /* -- Parameter Registry -- */
513
+ .param-row {
350
514
  display: flex;
351
- justify-content: space-between;
352
515
  align-items: center;
353
- padding: 6px 10px;
354
- border-radius: 4px;
355
- font-size: 12px;
516
+ justify-content: space-between;
517
+ padding: 6px 0;
518
+ border-bottom: 1px solid rgba(39,39,42,0.4);
519
+ font-family: 'JetBrains Mono', monospace;
520
+ font-size: 11px;
356
521
  }
357
522
 
358
- .registry-item:nth-child(odd) { background: rgba(255,255,255,0.02); }
359
- .registry-key { color: var(--text-secondary); font-family: var(--font-mono); }
360
- .registry-val { color: var(--accent); font-family: var(--font-mono); font-weight: 500; }
523
+ .param-row:last-child { border-bottom: none; }
361
524
 
362
- /* \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 */
363
- .advisor-banner {
525
+ .param-name {
526
+ color: var(--text-tertiary);
527
+ }
528
+
529
+ .param-val {
530
+ color: var(--accent);
531
+ font-variant-numeric: tabular-nums;
532
+ }
533
+
534
+ .param-changed {
535
+ color: var(--text-secondary);
536
+ font-size: 9px;
537
+ margin-left: 6px;
538
+ }
539
+
540
+ /* Ghost preview for pending recommendations */
541
+ .param-ghost {
364
542
  display: none;
365
- background: var(--warning-dim);
366
- border: 1px solid var(--warning);
367
543
  color: var(--warning);
368
- padding: 8px 16px;
369
- border-radius: 6px;
370
- font-size: 13px;
371
- font-weight: 500;
372
- align-items: center;
373
- gap: 8px;
544
+ font-size: 9px;
545
+ margin-left: 4px;
546
+ font-variant-numeric: tabular-nums;
374
547
  }
375
548
 
376
- .advisor-mode .advisor-banner { display: flex; }
549
+ .advisor-mode .param-ghost { display: inline; }
377
550
 
378
- .pending-pill {
379
- background: var(--warning);
380
- color: var(--bg-root);
551
+ .param-pending-label {
552
+ display: none;
553
+ color: var(--warning);
554
+ font-size: 9px;
555
+ margin-left: 6px;
556
+ }
557
+
558
+ .advisor-mode .param-pending-label { display: inline; }
559
+
560
+ .params-scroll {
561
+ overflow-y: auto;
562
+ max-height: 300px;
563
+ }
564
+
565
+ .params-scroll::-webkit-scrollbar { width: 4px; }
566
+ .params-scroll::-webkit-scrollbar-track { background: transparent; }
567
+ .params-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
568
+
569
+ /* -- Violations Table -- */
570
+ .violations-table {
571
+ width: 100%;
572
+ border-collapse: collapse;
381
573
  font-size: 11px;
574
+ font-family: 'JetBrains Mono', monospace;
575
+ }
576
+
577
+ .violations-table thead th {
578
+ text-align: left;
579
+ padding: 7px 8px;
580
+ border-bottom: 1px solid var(--border);
581
+ color: var(--text-secondary);
382
582
  font-weight: 600;
383
- padding: 1px 8px;
384
- border-radius: 10px;
385
- font-family: var(--font-mono);
583
+ font-size: 10px;
584
+ text-transform: uppercase;
585
+ letter-spacing: 0.06em;
586
+ cursor: pointer;
587
+ user-select: none;
588
+ white-space: nowrap;
589
+ position: sticky;
590
+ top: 0;
591
+ background: var(--bg-panel);
386
592
  }
387
593
 
388
- .advisor-btn {
389
- font-family: var(--font-mono);
390
- font-size: 11px;
391
- padding: 2px 10px;
594
+ .violations-table thead th:hover { color: var(--text-tertiary); }
595
+
596
+ .violations-table tbody td {
597
+ padding: 6px 8px;
598
+ border-bottom: 1px solid rgba(39,39,42,0.4);
599
+ color: var(--text-value);
600
+ font-variant-numeric: tabular-nums;
601
+ }
602
+
603
+ .violations-table tbody tr:hover td { background: rgba(39,39,42,0.3); }
604
+
605
+ .table-scroll {
606
+ overflow-y: auto;
607
+ max-height: 240px;
608
+ }
609
+
610
+ .table-scroll::-webkit-scrollbar { width: 4px; }
611
+ .table-scroll::-webkit-scrollbar-track { background: transparent; }
612
+ .table-scroll::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
613
+
614
+ .badge-applied { color: var(--accent); font-size: 10px; }
615
+ .badge-skipped { color: var(--text-secondary); font-size: 10px; }
616
+ .badge-rejected { color: var(--danger); font-size: 10px; opacity: 0.6; }
617
+
618
+ /* Pending badge in violations table (advisor mode) */
619
+ .badge-pending {
620
+ display: none;
621
+ color: var(--warning);
622
+ font-size: 10px;
623
+ cursor: pointer;
624
+ position: relative;
625
+ }
626
+
627
+ .advisor-mode .badge-pending { display: inline-flex; align-items: center; gap: 4px; }
628
+
629
+ .badge-pending:hover { text-decoration: underline; }
630
+
631
+ /* Pending dropdown in violations table */
632
+ .pending-dropdown {
633
+ display: none;
634
+ position: absolute;
635
+ bottom: 100%;
636
+ left: 0;
637
+ background: var(--bg-panel);
638
+ border: 1px solid var(--border);
392
639
  border-radius: 4px;
393
- border: none;
640
+ padding: 4px 0;
641
+ z-index: 20;
642
+ min-width: 120px;
643
+ box-shadow: 0 -4px 12px rgba(0,0,0,0.4);
644
+ }
645
+
646
+ .pending-dropdown.open { display: block; }
647
+
648
+ .pending-dropdown-item {
649
+ padding: 5px 12px;
650
+ font-family: 'JetBrains Mono', monospace;
651
+ font-size: 10px;
394
652
  cursor: pointer;
395
- font-weight: 500;
396
- transition: opacity 0.15s;
653
+ display: flex;
654
+ align-items: center;
655
+ gap: 6px;
656
+ white-space: nowrap;
397
657
  }
398
658
 
399
- .advisor-btn:hover { opacity: 0.85; }
400
- .advisor-btn.approve { background: var(--accent); color: var(--bg-root); }
401
- .advisor-btn.reject { background: var(--danger); color: #fff; }
659
+ .pending-dropdown-item:hover { background: rgba(39,39,42,0.5); }
660
+ .pending-dropdown-item.approve-item { color: var(--accent); }
661
+ .pending-dropdown-item.reject-item { color: var(--danger); }
402
662
 
403
- .advisor-actions { display: none; gap: 6px; margin-left: 8px; }
404
- .advisor-mode .advisor-actions { display: inline-flex; }
663
+ /* -- Bottom split row -- */
664
+ .split-row {
665
+ display: grid;
666
+ grid-template-columns: 1fr 1fr;
667
+ gap: 12px;
668
+ }
405
669
 
406
- /* \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 */
670
+ /* -- Empty state -- */
407
671
  .empty-state {
408
- color: var(--text-dim);
409
- font-size: 13px;
672
+ color: var(--text-secondary);
673
+ font-family: 'JetBrains Mono', monospace;
674
+ font-size: 11px;
410
675
  text-align: center;
411
- padding: 40px 20px;
676
+ padding: 20px;
677
+ }
678
+
679
+ /* -- Responsive -- */
680
+ @media (max-width: 768px) {
681
+ .split-row { grid-template-columns: 1fr; }
682
+ .header { flex-direction: column; align-items: flex-start; gap: 8px; }
683
+ .header-right { flex-wrap: wrap; gap: 12px; }
684
+ .dashboard { padding: 8px; gap: 8px; }
412
685
  }
413
686
 
414
- /* \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 */
687
+ /* -- Reduced motion -- */
415
688
  @media (prefers-reduced-motion: reduce) {
416
689
  .term-line { animation: none; opacity: 1; transform: none; }
417
690
  .live-dot { animation: none; }
418
691
  .persona-bar-fill { transition: none; }
692
+ .alert-card.resolved { transition: none; }
419
693
  }
420
694
  </style>
421
695
  </head>
422
696
  <body>
423
697
 
424
698
  <!-- Header -->
425
- <div class="header" id="header">
426
- <div class="header-brand">Agent<span>E</span> v1.6</div>
427
- <div class="kpi-row">
428
- <div class="kpi">Health <span class="kpi-value health-good" id="kpi-health">--</span></div>
429
- <div class="kpi">Mode <span class="kpi-value" id="kpi-mode">--</span></div>
430
- <div class="kpi">Tick <span class="kpi-value" id="kpi-tick">0</span></div>
431
- <div class="kpi">Uptime <span class="kpi-value" id="kpi-uptime">0s</span></div>
432
- <div class="kpi">Plans <span class="kpi-value" id="kpi-plans">0</span></div>
433
- <div class="live-dot" id="live-dot" title="WebSocket connected"></div>
699
+ <header class="header" id="header">
700
+ <div class="header-left">
701
+ <span class="header-logo">AgentE</span>
702
+ <span class="header-version">v1.7.2</span>
703
+ </div>
704
+ <div class="header-right">
705
+ <div class="kpi-pill">
706
+ <span class="label">Health</span>
707
+ <span class="value health-good" id="h-health">--</span>
708
+ </div>
709
+ <div class="kpi-pill">
710
+ <span class="label">Mode</span>
711
+ <span class="value mode-value-auto" id="h-mode">--</span>
712
+ </div>
713
+ <div class="kpi-pill">
714
+ <span class="label">Tick</span>
715
+ <span class="value" id="h-tick">0</span>
716
+ </div>
717
+ <div class="kpi-pill pending-pill" id="pending-pill">
718
+ <span class="label" style="color: var(--warning);">Pending</span>
719
+ <span class="value" style="color: var(--warning);" id="h-pending">0</span>
720
+ </div>
721
+ <div class="kpi-pill">
722
+ <span class="label">Uptime</span>
723
+ <span class="value" id="h-uptime">0s</span>
724
+ </div>
725
+ <div class="kpi-pill">
726
+ <div class="live-dot" id="live-dot" title="WebSocket connected"></div>
727
+ <span style="color: var(--accent); font-size: 11px;">LIVE</span>
728
+ </div>
434
729
  </div>
730
+ </header>
731
+
732
+ <!-- Advisor Banner -->
733
+ <div class="advisor-banner" id="advisor-banner">
734
+ ADVISOR MODE \\u2014 AgentE is waiting for your approval before applying changes
435
735
  </div>
436
736
 
437
- <div class="container" id="app">
438
- <!-- Advisor banner -->
439
- <div class="advisor-banner" id="advisor-banner">
440
- ADVISOR MODE \u2014 Recommendations require manual approval
441
- <span class="pending-pill" id="pending-count">0</span> pending
442
- </div>
737
+ <!-- Dashboard -->
738
+ <main class="dashboard" id="dashboard-root">
443
739
 
444
- <!-- Charts -->
445
- <div class="charts-grid">
446
- <div class="chart-box">
447
- <div class="chart-label">Economy Health</div>
448
- <div class="chart-value" id="cv-health">--</div>
449
- <canvas id="chart-health"></canvas>
740
+ <!-- Health & Metrics -->
741
+ <div class="panel charts-panel">
742
+ <div class="panel-header">
743
+ <span class="panel-title">Health & Metrics</span>
450
744
  </div>
451
- <div class="chart-box">
452
- <div class="chart-label">Gini Coefficient</div>
453
- <div class="chart-value" id="cv-gini">--</div>
454
- <canvas id="chart-gini"></canvas>
455
- </div>
456
- <div class="chart-box">
457
- <div class="chart-label">Net Flow</div>
458
- <div class="chart-value" id="cv-netflow">--</div>
459
- <canvas id="chart-netflow"></canvas>
460
- </div>
461
- <div class="chart-box">
462
- <div class="chart-label">Avg Satisfaction</div>
463
- <div class="chart-value" id="cv-satisfaction">--</div>
464
- <canvas id="chart-satisfaction"></canvas>
745
+ <div class="chart-row">
746
+ <div class="mini-chart">
747
+ <div class="mini-chart-label">Health Score</div>
748
+ <canvas id="chart-health"></canvas>
749
+ </div>
750
+ <div class="mini-chart">
751
+ <div class="mini-chart-label">Gini Coefficient</div>
752
+ <canvas id="chart-gini"></canvas>
753
+ </div>
754
+ <div class="mini-chart">
755
+ <div class="mini-chart-label">Net Flow</div>
756
+ <canvas id="chart-flow"></canvas>
757
+ </div>
758
+ <div class="mini-chart">
759
+ <div class="mini-chart-label">Avg Satisfaction</div>
760
+ <canvas id="chart-satisfaction"></canvas>
761
+ </div>
465
762
  </div>
466
763
  </div>
467
764
 
468
765
  <!-- Decision Feed -->
469
- <div class="panel">
470
- <div class="panel-title">Decision Feed</div>
471
- <div class="terminal" id="terminal"></div>
766
+ <div class="panel terminal-panel">
767
+ <div class="panel-header">
768
+ <span class="panel-title">Decision Feed</span>
769
+ <div class="panel-meta">
770
+ <span id="decision-count">0 decisions</span>
771
+ <span class="live-label"><div class="live-dot" style="width:5px;height:5px;"></div> LIVE</span>
772
+ </div>
773
+ </div>
774
+ <div class="panel-body">
775
+ <div class="terminal" id="terminal">
776
+ <div id="terminal-inner"></div>
777
+ </div>
778
+ </div>
472
779
  </div>
473
780
 
474
781
  <!-- Active Alerts -->
475
- <div class="panel">
476
- <div class="panel-title">Active Alerts</div>
477
- <div class="alerts-container" id="alerts-container">
478
- <div class="empty-state" id="alerts-empty">No active violations</div>
782
+ <div class="panel alerts-panel">
783
+ <div class="panel-header">
784
+ <span class="panel-title">Active Alerts</span>
785
+ <span class="panel-meta" id="alerts-count">All clear</span>
479
786
  </div>
787
+ <div class="alerts-scroll" id="alerts"></div>
480
788
  </div>
481
789
 
482
790
  <!-- Violation History -->
483
791
  <div class="panel">
484
- <div class="panel-title">Violation History</div>
485
- <div style="max-height:320px;overflow-y:auto">
792
+ <div class="panel-header">
793
+ <span class="panel-title">Violation History</span>
794
+ <span class="panel-meta">Last 100 decisions</span>
795
+ </div>
796
+ <div class="table-scroll">
486
797
  <table class="violations-table" id="violations-table">
487
798
  <thead>
488
799
  <tr>
489
800
  <th data-sort="tick">Tick</th>
490
801
  <th data-sort="principle">Principle</th>
491
- <th data-sort="severity">Severity</th>
802
+ <th data-sort="severity">Sev</th>
492
803
  <th data-sort="parameter">Parameter</th>
493
- <th data-sort="result">Result</th>
804
+ <th data-sort="result">Action</th>
805
+ <th>Change</th>
494
806
  </tr>
495
807
  </thead>
496
808
  <tbody id="violations-body"></tbody>
@@ -498,69 +810,88 @@ function getDashboardHtml() {
498
810
  </div>
499
811
  </div>
500
812
 
501
- <!-- Split: Personas + Registry -->
813
+ <!-- Persona Distribution + Parameters -->
502
814
  <div class="split-row">
503
- <div class="panel">
504
- <div class="panel-title">Persona Distribution</div>
505
- <div class="persona-bars" id="persona-bars">
815
+ <div class="panel persona-panel">
816
+ <div class="panel-header">
817
+ <span class="panel-title">Persona Distribution</span>
818
+ <span class="panel-meta" id="persona-count"></span>
819
+ </div>
820
+ <div id="persona-bars">
506
821
  <div class="empty-state">No persona data yet</div>
507
822
  </div>
508
823
  </div>
509
- <div class="panel">
510
- <div class="panel-title">Parameter Registry</div>
511
- <div class="registry-list" id="registry-list">
824
+
825
+ <div class="panel params-panel">
826
+ <div class="panel-header">
827
+ <span class="panel-title">Parameters</span>
828
+ <span class="panel-meta" id="params-count"></span>
829
+ </div>
830
+ <div class="params-scroll" id="params-list">
512
831
  <div class="empty-state">No parameters registered</div>
513
832
  </div>
514
833
  </div>
515
834
  </div>
516
- </div>
835
+
836
+ </main>
517
837
 
518
838
  <script>
519
839
  (function() {
520
840
  'use strict';
521
841
 
522
- // \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
523
- let ws = null;
524
- let reconnectDelay = 1000;
525
- const MAX_RECONNECT = 30000;
526
- let isAdvisor = false;
527
- let pendingDecisions = [];
528
- const MAX_TERMINAL_LINES = 80;
529
- const MAX_VIOLATIONS = 100;
530
- let violationSortKey = 'tick';
531
- let violationSortAsc = false;
532
- let violations = [];
842
+ // -- State --
843
+ var ws = null;
844
+ var reconnectDelay = 1000;
845
+ var MAX_RECONNECT = 30000;
846
+ var isAdvisor = false;
847
+ var pendingDecisions = [];
848
+ var MAX_TERMINAL_LINES = 80;
849
+ var MAX_VIOLATIONS = 100;
850
+ var violationSortKey = 'tick';
851
+ var violationSortAsc = false;
852
+ var violations = [];
853
+ var decisionCount = 0;
533
854
 
534
855
  // Chart instances
535
- let chartHealth, chartGini, chartNetflow, chartSatisfaction;
536
-
537
- // \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
538
- const $kpiHealth = document.getElementById('kpi-health');
539
- const $kpiMode = document.getElementById('kpi-mode');
540
- const $kpiTick = document.getElementById('kpi-tick');
541
- const $kpiUptime = document.getElementById('kpi-uptime');
542
- const $kpiPlans = document.getElementById('kpi-plans');
543
- const $liveDot = document.getElementById('live-dot');
544
- const $terminal = document.getElementById('terminal');
545
- const $alertsContainer = document.getElementById('alerts-container');
546
- const $alertsEmpty = document.getElementById('alerts-empty');
547
- const $violationsBody = document.getElementById('violations-body');
548
- const $personaBars = document.getElementById('persona-bars');
549
- const $registryList = document.getElementById('registry-list');
550
- const $pendingCount = document.getElementById('pending-count');
551
- const $app = document.getElementById('app');
552
-
553
- // \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
856
+ var chartHealth, chartGini, chartFlow, chartSatisfaction;
857
+
858
+ // Param state for registry display
859
+ var paramRegistry = [];
860
+ var paramValues = {};
861
+ var paramLastTick = {};
862
+ var paramPending = {};
863
+ var currentTick = 0;
864
+
865
+ // -- DOM refs --
866
+ var $hHealth = document.getElementById('h-health');
867
+ var $hMode = document.getElementById('h-mode');
868
+ var $hTick = document.getElementById('h-tick');
869
+ var $hUptime = document.getElementById('h-uptime');
870
+ var $hPending = document.getElementById('h-pending');
871
+ var $liveDot = document.getElementById('live-dot');
872
+ var $terminal = document.getElementById('terminal');
873
+ var $terminalInner = document.getElementById('terminal-inner');
874
+ var $alerts = document.getElementById('alerts');
875
+ var $alertsCount = document.getElementById('alerts-count');
876
+ var $violationsBody = document.getElementById('violations-body');
877
+ var $personaBars = document.getElementById('persona-bars');
878
+ var $paramsList = document.getElementById('params-list');
879
+ var $paramsCount = document.getElementById('params-count');
880
+ var $personaCount = document.getElementById('persona-count');
881
+ var $decisionCount = document.getElementById('decision-count');
882
+ var $dashboardRoot = document.getElementById('dashboard-root');
883
+
884
+ // -- Helpers --
554
885
  function esc(s) { return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;').replace(/'/g,'&#39;').replace(/\\\\/g,'&#92;'); }
555
886
  function pad(n, w) { return String(n).padStart(w || 4, ' '); }
556
- function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\u2014'; }
557
- function pct(n) { return typeof n === 'number' ? (n * 100).toFixed(0) + '%' : '\u2014'; }
887
+ function fmt(n) { return typeof n === 'number' ? n.toFixed(3) : '\\u2014'; }
888
+ function pct(n) { return typeof n === 'number' ? (n * 100).toFixed(0) + '%' : '\\u2014'; }
558
889
 
559
890
  function formatUptime(ms) {
560
- const s = Math.floor(ms / 1000);
891
+ var s = Math.floor(ms / 1000);
561
892
  if (s < 60) return s + 's';
562
893
  if (s < 3600) return Math.floor(s / 60) + 'm ' + (s % 60) + 's';
563
- const h = Math.floor(s / 3600);
894
+ var h = Math.floor(s / 3600);
564
895
  return h + 'h ' + Math.floor((s % 3600) / 60) + 'm';
565
896
  }
566
897
 
@@ -571,34 +902,63 @@ function getDashboardHtml() {
571
902
  }
572
903
 
573
904
  function sevClass(s) {
905
+ if (s >= 7) return 'high';
906
+ if (s >= 4) return 'med';
907
+ return 'low';
908
+ }
909
+
910
+ function sevCardClass(s) {
574
911
  if (s >= 7) return 'sev-high';
575
912
  if (s >= 4) return 'sev-med';
576
913
  return 'sev-low';
577
914
  }
578
915
 
579
- // \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
580
- const chartOpts = {
916
+ function personaColor(name) {
917
+ var n = (name || '').toLowerCase();
918
+ if (n === 'atrisk' || n === 'at_risk' || n === 'dormant') return 'var(--danger)';
919
+ if (n === 'spender' || n === 'newentrant' || n === 'new_entrant' || n === 'passive') return 'var(--warning)';
920
+ return 'var(--accent)';
921
+ }
922
+
923
+ // -- Chart setup --
924
+ var chartOpts = {
581
925
  responsive: true,
582
926
  maintainAspectRatio: false,
583
- animation: { duration: 300 },
584
- plugins: { legend: { display: false } },
927
+ animation: false,
928
+ plugins: {
929
+ legend: { display: false },
930
+ tooltip: {
931
+ backgroundColor: '#18181b',
932
+ titleColor: '#a1a1aa',
933
+ bodyColor: '#d4d4d8',
934
+ titleFont: { family: 'JetBrains Mono', size: 10 },
935
+ bodyFont: { family: 'JetBrains Mono', size: 11 },
936
+ borderColor: '#27272a',
937
+ borderWidth: 1,
938
+ padding: 8,
939
+ }
940
+ },
585
941
  scales: {
586
942
  x: { display: false },
587
943
  y: {
588
- ticks: { color: '#71717a', font: { family: "'JetBrains Mono'", size: 10 } },
589
- grid: { color: 'rgba(63,63,70,0.3)' },
944
+ grid: { color: 'rgba(39,39,42,0.5)', drawBorder: false },
945
+ ticks: {
946
+ color: '#52525b',
947
+ font: { family: 'JetBrains Mono', size: 9 },
948
+ maxTicksLimit: 3,
949
+ },
590
950
  border: { display: false },
591
951
  }
592
952
  },
593
953
  elements: {
594
- point: { radius: 0 },
954
+ point: { radius: 0, hoverRadius: 3, backgroundColor: '#22c55e' },
595
955
  line: { borderWidth: 1.5, tension: 0.3 },
596
956
  }
597
957
  };
598
958
 
599
959
  function makeChart(id, color, minY, maxY) {
600
- const ctx = document.getElementById(id).getContext('2d');
601
- const opts = JSON.parse(JSON.stringify(chartOpts));
960
+ var ctx = document.getElementById(id).getContext('2d');
961
+ var opts = JSON.parse(JSON.stringify(chartOpts));
602
962
  if (minY !== undefined) opts.scales.y.min = minY;
603
963
  if (maxY !== undefined) opts.scales.y.max = maxY;
604
964
  return new Chart(ctx, {
@@ -619,7 +979,7 @@ function getDashboardHtml() {
619
979
  function initCharts() {
620
980
  chartHealth = makeChart('chart-health', '#22c55e', 0, 100);
621
981
  chartGini = makeChart('chart-gini', '#eab308', 0, 1);
622
- chartNetflow = makeChart('chart-netflow', '#3b82f6');
982
+ chartFlow = makeChart('chart-flow', '#3b82f6');
623
983
  chartSatisfaction = makeChart('chart-satisfaction', '#22c55e', 0, 100);
624
984
  }
625
985
 
@@ -629,178 +989,278 @@ function getDashboardHtml() {
629
989
  chart.update('none');
630
990
  }
631
991
 
632
- // \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
992
+ // -- Terminal --
633
993
  function addTerminalLine(html) {
634
- const el = document.createElement('div');
994
+ var el = document.createElement('div');
635
995
  el.className = 'term-line';
636
996
  el.innerHTML = html;
637
- $terminal.appendChild(el);
638
- while ($terminal.children.length > MAX_TERMINAL_LINES) {
639
- $terminal.removeChild($terminal.firstChild);
997
+ $terminalInner.appendChild(el);
998
+ while ($terminalInner.children.length > MAX_TERMINAL_LINES) {
999
+ $terminalInner.removeChild($terminalInner.firstChild);
640
1000
  }
641
1001
  $terminal.scrollTop = $terminal.scrollHeight;
642
1002
  }
643
1003
 
644
1004
  function decisionToTerminal(d) {
645
- const resultIcon = d.result === 'applied'
646
- ? '<span class="t-ok">\\u2705 </span>'
1005
+ var resultIcon = d.result === 'applied'
1006
+ ? '<span class="t-check">\\u2705 </span>'
647
1007
  : d.result === 'rejected'
648
1008
  ? '<span class="t-fail">\\u274c </span>'
649
- : '<span class="t-skip">\\u23f8 </span>';
1009
+ : d.result === 'skipped_override'
1010
+ ? '<span class="t-pending-icon">\\u23f3 </span>'
1011
+ : '<span class="t-skip">\\u23f8 </span>';
650
1012
 
651
- const principle = d.diagnosis?.principle || {};
652
- const plan = d.plan || {};
653
- const severity = d.diagnosis?.violation?.severity ?? '?';
654
- const confidence = d.diagnosis?.violation?.confidence;
655
- const confStr = confidence != null ? (confidence * 100).toFixed(0) + '%' : '?';
1013
+ var principle = d.diagnosis?.principle || {};
1014
+ var plan = d.plan || {};
1015
+ var severity = d.diagnosis?.violation?.severity ?? '?';
1016
+ var confidence = d.diagnosis?.violation?.confidence;
1017
+ var confStr = confidence != null ? (confidence * 100).toFixed(0) + '%' : '?';
656
1018
 
657
- let advisorBtns = '';
1019
+ var advisorBtns = '';
658
1020
  if (isAdvisor && d.result === 'skipped_override') {
659
- advisorBtns = '<span class="advisor-actions">'
660
- + '<button class="advisor-btn approve" onclick="window._approve(\\'' + esc(d.id) + '\\')">[Approve]</button>'
661
- + '<button class="advisor-btn reject" onclick="window._reject(\\'' + esc(d.id) + '\\')">[Reject]</button>'
1021
+ advisorBtns = '<span class="advisor-btn-group" data-id="' + esc(d.id) + '">'
1022
+ + '<button class="advisor-btn approve-btn" data-action="approve" data-id="' + esc(d.id) + '">&#10003; Approve</button>'
1023
+ + '<button class="advisor-btn reject-btn" data-action="reject" data-id="' + esc(d.id) + '">&#10005; Reject</button>'
662
1024
  + '</span>';
663
1025
  }
664
1026
 
665
1027
  return '<span class="t-tick">[Tick ' + pad(d.tick) + ']</span> '
666
1028
  + resultIcon
667
- + '<span class="t-principle">[' + esc(principle.id || '?') + '] ' + esc(principle.name || '') + ':</span> '
668
- + '<span class="t-param">' + esc(plan.parameter || '\u2014') + ' </span>'
1029
+ + '<span class="t-principle">' + esc(principle.name || '') + ':</span> '
1030
+ + '<span class="t-param">' + esc(plan.parameter || '\\u2014') + ' </span>'
669
1031
  + '<span class="t-old">' + fmt(plan.currentValue) + '</span>'
670
1032
  + '<span class="t-arrow"> \\u2192 </span>'
671
- + '<span class="t-new">' + fmt(plan.targetValue) + '</span>'
672
- + '<span class="t-meta"> severity ' + severity + '/10, confidence ' + confStr + '</span>'
1033
+ + (d.result === 'skipped_override'
1034
+ ? '<span class="t-pending-val">' + fmt(plan.targetValue) + '</span>'
1035
+ : '<span class="t-new">' + fmt(plan.targetValue) + '</span>')
1036
+ + '<span class="t-meta"> sev ' + severity + ', conf ' + confStr + '</span>'
673
1037
  + advisorBtns;
674
1038
  }
675
1039
 
676
- // \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
1040
+ // -- Alerts --
677
1041
  function renderAlerts(alerts) {
678
1042
  if (!alerts || alerts.length === 0) {
679
- $alertsContainer.innerHTML = '<div class="empty-state">No active violations</div>';
1043
+ $alerts.innerHTML = '<div class="empty-state">No active violations. Economy is healthy.</div>';
1044
+ $alertsCount.textContent = 'All clear';
680
1045
  return;
681
1046
  }
682
- const sorted = [...alerts].sort((a, b) => (b.severity || 0) - (a.severity || 0));
683
- $alertsContainer.innerHTML = sorted.map(function(a) {
684
- const sev = a.severity || a.violation?.severity || 0;
685
- const sc = sevClass(sev);
686
- const name = a.principleName || a.principle?.name || '?';
687
- const pid = a.principleId || a.principle?.id || '?';
688
- const reason = a.reasoning || a.violation?.suggestedAction?.reasoning || '';
689
- return '<div class="alert-card">'
690
- + '<span class="alert-severity ' + sc + '">' + sev + '/10</span>'
691
- + '<div class="alert-body">'
692
- + '<div class="alert-principle">[' + esc(pid) + '] ' + esc(name) + '</div>'
693
- + '<div class="alert-reason">' + esc(reason) + '</div>'
694
- + '</div></div>';
1047
+ var sorted = alerts.slice().sort(function(a, b) { return (b.severity || 0) - (a.severity || 0); });
1048
+ $alertsCount.textContent = sorted.length + ' violation' + (sorted.length !== 1 ? 's' : '');
1049
+
1050
+ $alerts.innerHTML = sorted.map(function(a) {
1051
+ var sev = a.severity || a.violation?.severity || 0;
1052
+ var sc = sevClass(sev);
1053
+ var cardCls = sevCardClass(sev);
1054
+ var name = a.principleName || a.principle?.name || '?';
1055
+ var pid = a.principleId || a.principle?.id || '?';
1056
+ var reason = a.reasoning || a.violation?.suggestedAction?.reasoning || '';
1057
+ var suggestion = a.suggestion || '';
1058
+
1059
+ var hasPending = isAdvisor && pendingDecisions.some(function(pd) {
1060
+ return pd.principleId === pid || (pd.diagnosis?.principle?.id === pid);
1061
+ });
1062
+
1063
+ var btns = '';
1064
+ if (hasPending) {
1065
+ btns = '<button class="alert-approve-btn" data-action="approve-alert" data-principle="' + esc(pid) + '">&#10003; Approve Fix</button>'
1066
+ + '<button class="alert-reject-btn" data-action="reject-alert" data-principle="' + esc(pid) + '">&#10007; Reject</button>';
1067
+ }
1068
+
1069
+ return '<div class="alert-card ' + cardCls + '" data-principle-id="' + esc(pid) + '">'
1070
+ + '<div class="alert-top">'
1071
+ + '<span class="sev-badge ' + sc + '">' + sev + '</span>'
1072
+ + '<span class="alert-principle-name">[' + esc(pid) + '] ' + esc(name) + '</span>'
1073
+ + '</div>'
1074
+ + (reason ? '<div class="alert-evidence">' + esc(reason) + '</div>' : '')
1075
+ + (suggestion ? '<div class="alert-suggestion">Suggested: ' + esc(suggestion) + '</div>' : '')
1076
+ + btns
1077
+ + '</div>';
695
1078
  }).join('');
696
1079
  }
697
1080
 
698
- // \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
1081
+ // -- Violations table --
699
1082
  function addViolation(d) {
1083
+ var plan = d.plan || {};
700
1084
  violations.push({
701
1085
  tick: d.tick,
702
1086
  principle: (d.diagnosis?.principle?.id || '?') + ' ' + (d.diagnosis?.principle?.name || ''),
703
1087
  severity: d.diagnosis?.violation?.severity || 0,
704
- parameter: d.plan?.parameter || '\u2014',
1088
+ parameter: plan.parameter || '\\u2014',
705
1089
  result: d.result,
1090
+ currentValue: plan.currentValue,
1091
+ targetValue: plan.targetValue,
1092
+ decisionId: d.id,
706
1093
  });
707
1094
  if (violations.length > MAX_VIOLATIONS) violations.shift();
1095
+ decisionCount = violations.length;
1096
+ $decisionCount.textContent = decisionCount + ' decisions';
708
1097
  renderViolations();
709
1098
  }
710
1099
 
711
1100
  function renderViolations() {
712
- const sorted = [...violations].sort(function(a, b) {
713
- const va = a[violationSortKey], vb = b[violationSortKey];
1101
+ var sorted = violations.slice().sort(function(a, b) {
1102
+ var va = a[violationSortKey], vb = b[violationSortKey];
714
1103
  if (va < vb) return violationSortAsc ? -1 : 1;
715
1104
  if (va > vb) return violationSortAsc ? 1 : -1;
716
1105
  return 0;
717
1106
  });
718
1107
  $violationsBody.innerHTML = sorted.map(function(v) {
1108
+ var isPending = v.result === 'skipped_override';
1109
+
1110
+ var actionHtml;
1111
+ if (isPending && isAdvisor) {
1112
+ actionHtml = '<span class="badge-pending" data-action="toggle-pending" data-id="' + esc(v.decisionId || '') + '">'
1113
+ + 'Pending \\u25BE'
1114
+ + '<div class="pending-dropdown">'
1115
+ + '<div class="pending-dropdown-item approve-item" data-action="approve" data-id="' + esc(v.decisionId || '') + '">&#10003; Approve</div>'
1116
+ + '<div class="pending-dropdown-item reject-item" data-action="reject" data-id="' + esc(v.decisionId || '') + '">&#10007; Reject</div>'
1117
+ + '</div>'
1118
+ + '</span>';
1119
+ } else if (v.result === 'applied') {
1120
+ actionHtml = '<span class="badge-applied">Applied</span>';
1121
+ } else if (v.result === 'rejected') {
1122
+ actionHtml = '<span class="badge-rejected">Rejected</span>';
1123
+ } else {
1124
+ actionHtml = '<span class="badge-skipped">' + esc(v.result || 'Skipped') + '</span>';
1125
+ }
1126
+
1127
+ var changeHtml = '';
1128
+ if (v.currentValue != null && v.targetValue != null) {
1129
+ var valColor = isPending && isAdvisor ? 'var(--warning)' : 'var(--accent)';
1130
+ changeHtml = '<span style="color:var(--text-value)">' + fmt(v.currentValue) + '</span>'
1131
+ + '<span style="color:var(--info)"> \\u2192 </span>'
1132
+ + '<span style="color:' + valColor + '">' + fmt(v.targetValue) + '</span>';
1133
+ }
1134
+
1135
+ var sevColor = v.severity >= 7 ? 'var(--danger)' : v.severity >= 4 ? 'var(--warning)' : 'var(--accent)';
1136
+
719
1137
  return '<tr>'
720
- + '<td>' + v.tick + '</td>'
721
- + '<td style="color:var(--text-primary);font-family:var(--font-sans)">' + esc(v.principle) + '</td>'
722
- + '<td><span class="alert-severity ' + sevClass(v.severity) + '">' + v.severity + '</span></td>'
723
- + '<td>' + esc(v.parameter) + '</td>'
724
- + '<td>' + esc(v.result) + '</td>'
1138
+ + '<td style="color:var(--text-secondary)">' + v.tick + '</td>'
1139
+ + '<td style="color:var(--text-value)">' + esc(v.principle) + '</td>'
1140
+ + '<td style="color:' + sevColor + '">' + v.severity + '</td>'
1141
+ + '<td style="color:var(--text-tertiary)">' + esc(v.parameter) + '</td>'
1142
+ + '<td class="action-cell">' + actionHtml + '</td>'
1143
+ + '<td>' + changeHtml + '</td>'
725
1144
  + '</tr>';
726
1145
  }).join('');
727
1146
  }
728
1147
 
729
1148
  // Table sorting
730
- document.querySelectorAll('.violations-table th').forEach(function(th) {
1149
+ document.querySelectorAll('.violations-table th[data-sort]').forEach(function(th) {
731
1150
  th.addEventListener('click', function() {
732
- const key = th.dataset.sort;
1151
+ var key = th.dataset.sort;
733
1152
  if (violationSortKey === key) violationSortAsc = !violationSortAsc;
734
1153
  else { violationSortKey = key; violationSortAsc = true; }
735
1154
  renderViolations();
736
1155
  });
737
1156
  });
738
1157
 
739
- // \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
1158
+ // -- Personas --
740
1159
  function renderPersonas(dist) {
741
1160
  if (!dist || Object.keys(dist).length === 0) {
742
1161
  $personaBars.innerHTML = '<div class="empty-state">No persona data yet</div>';
1162
+ $personaCount.textContent = '';
743
1163
  return;
744
1164
  }
745
- const total = Object.values(dist).reduce(function(s, v) { return s + v; }, 0);
746
- const entries = Object.entries(dist).sort(function(a, b) { return b[1] - a[1]; });
1165
+ var total = Object.values(dist).reduce(function(s, v) { return s + v; }, 0);
1166
+ $personaCount.textContent = total + ' agents';
1167
+ var entries = Object.entries(dist).sort(function(a, b) { return b[1] - a[1]; });
747
1168
  $personaBars.innerHTML = entries.map(function(e) {
748
- const pctVal = total > 0 ? (e[1] / total * 100) : 0;
1169
+ var pctVal = total > 0 ? (e[1] / total * 100) : 0;
1170
+ var color = personaColor(e[0]);
749
1171
  return '<div class="persona-row">'
750
- + '<div class="persona-label">' + esc(e[0]) + '</div>'
751
- + '<div class="persona-bar-track"><div class="persona-bar-fill" style="width:' + pctVal + '%"></div></div>'
752
- + '<div class="persona-pct">' + pctVal.toFixed(0) + '%</div>'
1172
+ + '<span class="persona-label">' + esc(e[0]) + '</span>'
1173
+ + '<div class="persona-bar-track"><div class="persona-bar-fill" style="width:' + pctVal.toFixed(0) + '%;background:' + color + ';"></div></div>'
1174
+ + '<span class="persona-pct">' + pctVal.toFixed(0) + '%</span>'
753
1175
  + '</div>';
754
1176
  }).join('');
755
1177
  }
756
1178
 
757
- // \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
758
- function renderRegistry(principles) {
759
- if (!principles || principles.length === 0) {
760
- $registryList.innerHTML = '<div class="empty-state">No parameters registered</div>';
1179
+ // -- Parameters --
1180
+ function renderParams(principles, registryValues) {
1181
+ if ((!principles || principles.length === 0) && Object.keys(paramValues).length === 0) {
1182
+ $paramsList.innerHTML = '<div class="empty-state">No parameters registered</div>';
1183
+ $paramsCount.textContent = '';
761
1184
  return;
762
1185
  }
763
- $registryList.innerHTML = principles.slice(0, 30).map(function(p) {
764
- return '<div class="registry-item">'
765
- + '<span class="registry-key">[' + esc(p.id) + ']</span>'
766
- + '<span class="registry-val">' + esc(p.name) + '</span>'
767
- + '</div>';
768
- }).join('');
1186
+
1187
+ // If we have actual parameter values from API, show those
1188
+ if (registryValues && Object.keys(registryValues).length > 0) {
1189
+ var entries = Object.entries(registryValues);
1190
+ $paramsCount.textContent = entries.length + ' tracked';
1191
+ $paramsList.innerHTML = entries.map(function(e) {
1192
+ var key = e[0];
1193
+ var val = e[1];
1194
+ var ticksAgo = currentTick - (paramLastTick[key] || 0);
1195
+ var agoText = ticksAgo <= 0 ? '' : ticksAgo <= 5 ? 'just now' : ticksAgo + ' ticks ago';
1196
+ var pending = paramPending[key];
1197
+
1198
+ if (pending && isAdvisor) {
1199
+ return '<div class="param-row">'
1200
+ + '<span class="param-name">' + esc(key) + '</span>'
1201
+ + '<span>'
1202
+ + '<span class="param-val">' + fmt(val) + '</span>'
1203
+ + '<span class="param-ghost" style="display:inline;"> \\u2192 ' + fmt(pending.proposedVal) + '?</span>'
1204
+ + '<span class="param-pending-label" style="display:inline;">pending</span>'
1205
+ + '</span>'
1206
+ + '</div>';
1207
+ }
1208
+ return '<div class="param-row">'
1209
+ + '<span class="param-name">' + esc(key) + '</span>'
1210
+ + '<span><span class="param-val">' + fmt(val) + '</span>'
1211
+ + (agoText ? '<span class="param-changed">' + agoText + '</span>' : '')
1212
+ + '</span>'
1213
+ + '</div>';
1214
+ }).join('');
1215
+ return;
1216
+ }
1217
+
1218
+ // Fallback: show principle names (legacy behavior)
1219
+ if (principles && principles.length > 0) {
1220
+ $paramsCount.textContent = principles.length + ' registered';
1221
+ $paramsList.innerHTML = principles.slice(0, 30).map(function(p) {
1222
+ return '<div class="param-row">'
1223
+ + '<span class="param-name">[' + esc(p.id) + ']</span>'
1224
+ + '<span class="param-val">' + esc(p.name) + '</span>'
1225
+ + '</div>';
1226
+ }).join('');
1227
+ }
769
1228
  }
770
1229
 
771
- // \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
1230
+ // -- KPI update --
772
1231
  function updateKPIs(data) {
773
1232
  if (data.health != null) {
774
- $kpiHealth.textContent = data.health + '/100';
775
- $kpiHealth.className = 'kpi-value ' + healthClass(data.health);
776
- document.getElementById('cv-health').textContent = data.health + '/100';
1233
+ $hHealth.textContent = data.health + '/100';
1234
+ $hHealth.className = 'value ' + healthClass(data.health);
777
1235
  }
778
1236
  if (data.mode != null) {
779
- $kpiMode.textContent = data.mode;
1237
+ $hMode.textContent = data.mode;
780
1238
  isAdvisor = data.mode === 'advisor';
781
- $app.classList.toggle('advisor-mode', isAdvisor);
1239
+ $hMode.className = 'value ' + (isAdvisor ? 'mode-value-advisor' : 'mode-value-auto');
1240
+ document.body.classList.toggle('advisor-mode', isAdvisor);
1241
+ }
1242
+ if (data.tick != null) {
1243
+ $hTick.textContent = data.tick;
1244
+ currentTick = data.tick;
1245
+ }
1246
+ if (data.uptime != null) $hUptime.textContent = formatUptime(data.uptime);
1247
+ if (data.pendingCount != null || data.activePlans != null) {
1248
+ var count = data.pendingCount || data.activePlans || 0;
1249
+ $hPending.textContent = count;
782
1250
  }
783
- if (data.tick != null) $kpiTick.textContent = data.tick;
784
- if (data.uptime != null) $kpiUptime.textContent = formatUptime(data.uptime);
785
- if (data.activePlans != null) $kpiPlans.textContent = data.activePlans;
786
1251
  }
787
1252
 
788
- // \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
1253
+ // -- Metrics history --
789
1254
  function updateChartsFromHistory(history) {
790
1255
  if (!history || history.length === 0) return;
791
- const ticks = history.map(function(h) { return h.tick; });
1256
+ var ticks = history.map(function(h) { return h.tick; });
792
1257
  updateChart(chartHealth, ticks, history.map(function(h) { return h.health; }));
793
1258
  updateChart(chartGini, ticks, history.map(function(h) { return h.giniCoefficient; }));
794
- updateChart(chartNetflow, ticks, history.map(function(h) { return h.netFlow; }));
1259
+ updateChart(chartFlow, ticks, history.map(function(h) { return h.netFlow; }));
795
1260
  updateChart(chartSatisfaction, ticks, history.map(function(h) { return h.avgSatisfaction; }));
796
-
797
- const last = history[history.length - 1];
798
- document.getElementById('cv-gini').textContent = last.giniCoefficient.toFixed(3);
799
- document.getElementById('cv-netflow').textContent = last.netFlow.toFixed(1);
800
- document.getElementById('cv-satisfaction').textContent = last.avgSatisfaction.toFixed(0) + '/100';
801
1261
  }
802
1262
 
803
- // \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
1263
+ // -- API calls --
804
1264
  function fetchJSON(path) {
805
1265
  return fetch(path).then(function(r) { return r.json(); });
806
1266
  }
@@ -835,19 +1295,22 @@ function getDashboardHtml() {
835
1295
  }).catch(function() {});
836
1296
 
837
1297
  fetchJSON('/principles').then(function(data) {
838
- if (data.principles) renderRegistry(data.principles);
1298
+ if (data.principles) {
1299
+ paramRegistry = data.principles;
1300
+ renderParams(data.principles, paramValues);
1301
+ }
839
1302
  }).catch(function() {});
840
1303
 
841
1304
  fetchJSON('/pending').then(function(data) {
842
1305
  if (data.pending) {
843
1306
  pendingDecisions = data.pending;
844
- $pendingCount.textContent = data.count || 0;
1307
+ $hPending.textContent = data.count || 0;
845
1308
  }
846
1309
  }).catch(function() {});
847
1310
  }
848
1311
 
849
- // \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
850
- let pollInterval = null;
1312
+ // -- Polling fallback --
1313
+ var pollInterval = null;
851
1314
 
852
1315
  function startPolling() {
853
1316
  if (pollInterval) return;
@@ -864,9 +1327,9 @@ function getDashboardHtml() {
864
1327
  if (pollInterval) { clearInterval(pollInterval); pollInterval = null; }
865
1328
  }
866
1329
 
867
- // \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
1330
+ // -- WebSocket --
868
1331
  function connectWS() {
869
- const proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
1332
+ var proto = location.protocol === 'https:' ? 'wss:' : 'ws:';
870
1333
  ws = new WebSocket(proto + '//' + location.host);
871
1334
 
872
1335
  ws.onopen = function() {
@@ -874,13 +1337,12 @@ function getDashboardHtml() {
874
1337
  $liveDot.classList.remove('disconnected');
875
1338
  $liveDot.title = 'WebSocket connected';
876
1339
  stopPolling();
877
- // Request fresh health
878
1340
  ws.send(JSON.stringify({ type: 'health' }));
879
1341
  };
880
1342
 
881
1343
  ws.onclose = function() {
882
1344
  $liveDot.classList.add('disconnected');
883
- $liveDot.title = 'WebSocket disconnected \u2014 reconnecting...';
1345
+ $liveDot.title = 'WebSocket disconnected \\u2014 reconnecting...';
884
1346
  startPolling();
885
1347
  setTimeout(connectWS, reconnectDelay);
886
1348
  reconnectDelay = Math.min(reconnectDelay * 1.5, MAX_RECONNECT);
@@ -896,7 +1358,6 @@ function getDashboardHtml() {
896
1358
  case 'tick_result':
897
1359
  updateKPIs({ health: msg.health, tick: msg.tick });
898
1360
  if (msg.alerts) renderAlerts(msg.alerts);
899
- // Refresh charts
900
1361
  fetchJSON('/metrics').then(function(data) {
901
1362
  if (data.history) updateChartsFromHistory(data.history);
902
1363
  if (data.latest) renderPersonas(data.latest.personaDistribution);
@@ -912,32 +1373,90 @@ function getDashboardHtml() {
912
1373
  pendingDecisions = pendingDecisions.filter(function(d) {
913
1374
  return d.id !== msg.decisionId;
914
1375
  });
915
- $pendingCount.textContent = pendingDecisions.length;
1376
+ $hPending.textContent = pendingDecisions.length;
916
1377
  }
917
1378
  break;
918
1379
  }
919
1380
  };
920
1381
  }
921
1382
 
922
- // \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
923
- window._approve = function(id) {
924
- postJSON('/approve', { decisionId: id }).then(function(data) {
925
- if (data.ok) {
926
- addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-ok">\\u2705 Approved ' + esc(id) + '</span>');
1383
+ // -- Advisor actions (event delegation) --
1384
+ document.addEventListener('click', function(e) {
1385
+ var btn = e.target.closest('[data-action]');
1386
+ if (!btn) return;
1387
+ var action = btn.getAttribute('data-action');
1388
+ var id = btn.getAttribute('data-id');
1389
+ var principleId = btn.getAttribute('data-principle');
1390
+
1391
+ // Toggle pending dropdown
1392
+ if (action === 'toggle-pending') {
1393
+ var dropdown = btn.querySelector('.pending-dropdown');
1394
+ if (!dropdown) return;
1395
+ document.querySelectorAll('.pending-dropdown.open').forEach(function(d) {
1396
+ if (d !== dropdown) d.classList.remove('open');
1397
+ });
1398
+ dropdown.classList.toggle('open');
1399
+ e.stopPropagation();
1400
+ return;
1401
+ }
1402
+
1403
+ // Approve from alert card
1404
+ if (action === 'approve-alert' && principleId) {
1405
+ var pd = pendingDecisions.find(function(d) {
1406
+ return d.principleId === principleId || (d.diagnosis?.principle?.id === principleId);
1407
+ });
1408
+ if (pd) {
1409
+ postJSON('/approve', { decisionId: pd.id }).then(function(data) {
1410
+ if (data.ok) {
1411
+ addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-check">\\u2705 Approved ' + esc(pd.id) + '</span>');
1412
+ }
1413
+ }).catch(function() {});
927
1414
  }
928
- }).catch(function() {});
929
- };
1415
+ return;
1416
+ }
930
1417
 
931
- window._reject = function(id) {
932
- var reason = prompt('Rejection reason (optional):');
933
- postJSON('/reject', { decisionId: id, reason: reason || undefined }).then(function(data) {
934
- if (data.ok) {
935
- addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-fail">\\u274c Rejected ' + esc(id) + '</span>');
1418
+ // Reject from alert card
1419
+ if (action === 'reject-alert' && principleId) {
1420
+ var pd2 = pendingDecisions.find(function(d) {
1421
+ return d.principleId === principleId || (d.diagnosis?.principle?.id === principleId);
1422
+ });
1423
+ if (pd2) {
1424
+ var reason = prompt('Rejection reason (optional):');
1425
+ postJSON('/reject', { decisionId: pd2.id, reason: reason || undefined }).then(function(data) {
1426
+ if (data.ok) {
1427
+ addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-fail">\\u274c Rejected ' + esc(pd2.id) + '</span>');
1428
+ }
1429
+ }).catch(function() {});
936
1430
  }
937
- }).catch(function() {});
938
- };
1431
+ return;
1432
+ }
1433
+
1434
+ if (!id) return;
1435
+
1436
+ if (action === 'approve') {
1437
+ postJSON('/approve', { decisionId: id }).then(function(data) {
1438
+ if (data.ok) {
1439
+ addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-check">\\u2705 Approved ' + esc(id) + '</span>');
1440
+ }
1441
+ }).catch(function() {});
1442
+ } else if (action === 'reject') {
1443
+ var reason2 = prompt('Rejection reason (optional):');
1444
+ postJSON('/reject', { decisionId: id, reason: reason2 || undefined }).then(function(data) {
1445
+ if (data.ok) {
1446
+ addTerminalLine('<span class="t-tick">[Advisor]</span> <span class="t-fail">\\u274c Rejected ' + esc(id) + '</span>');
1447
+ }
1448
+ }).catch(function() {});
1449
+ }
1450
+ });
1451
+
1452
+ // Close dropdowns on outside click
1453
+ document.addEventListener('click', function(e) {
1454
+ if (!e.target.closest('.badge-pending')) {
1455
+ document.querySelectorAll('.pending-dropdown.open').forEach(function(d) { d.classList.remove('open'); });
1456
+ }
1457
+ });
939
1458
 
940
- // \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
1459
+ // -- Init --
941
1460
  initCharts();
942
1461
  loadInitialData();
943
1462
  connectWS();
@@ -1327,6 +1846,7 @@ function createRouteHandler(server) {
1327
1846
  }
1328
1847
 
1329
1848
  // src/websocket.ts
1849
+ import { timingSafeEqual as timingSafeEqual2 } from "crypto";
1330
1850
  import { WebSocketServer, WebSocket } from "ws";
1331
1851
  import { validateEconomyState as validateEconomyState2 } from "@agent-e/core";
1332
1852
  function send(ws, data) {
@@ -1377,7 +1897,7 @@ function createWebSocketHandler(httpServer, server) {
1377
1897
  if (server.apiKey) {
1378
1898
  const url = new URL(req.url ?? "/", `http://${req.headers.host ?? "localhost"}`);
1379
1899
  const token = url.searchParams.get("token") ?? req.headers["authorization"]?.replace("Bearer ", "");
1380
- if (token !== server.apiKey) {
1900
+ if (!token || token.length !== server.apiKey.length || !timingSafeEqual2(Buffer.from(token), Buffer.from(server.apiKey))) {
1381
1901
  ws.close(1008, "Unauthorized");
1382
1902
  return;
1383
1903
  }
@@ -1686,4 +2206,4 @@ var AgentEServer = class {
1686
2206
  export {
1687
2207
  AgentEServer
1688
2208
  };
1689
- //# sourceMappingURL=chunk-O2UFQVI2.mjs.map
2209
+ //# sourceMappingURL=chunk-AWYCK646.mjs.map