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