@agent-e/server 1.7.2 → 1.8.0

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