decision_agent 0.1.1 → 0.1.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.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +234 -919
  3. data/bin/decision_agent +5 -5
  4. data/lib/decision_agent/agent.rb +19 -26
  5. data/lib/decision_agent/audit/null_adapter.rb +1 -2
  6. data/lib/decision_agent/decision.rb +3 -1
  7. data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
  8. data/lib/decision_agent/dsl/rule_parser.rb +4 -6
  9. data/lib/decision_agent/dsl/schema_validator.rb +27 -31
  10. data/lib/decision_agent/errors.rb +21 -6
  11. data/lib/decision_agent/evaluation.rb +3 -1
  12. data/lib/decision_agent/evaluation_validator.rb +78 -0
  13. data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
  14. data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
  15. data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
  16. data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
  17. data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
  18. data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
  19. data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
  20. data/lib/decision_agent/monitoring/metrics_collector.rb +278 -0
  21. data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
  22. data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
  23. data/lib/decision_agent/replay/replay.rb +12 -22
  24. data/lib/decision_agent/scoring/base.rb +1 -1
  25. data/lib/decision_agent/scoring/consensus.rb +5 -5
  26. data/lib/decision_agent/scoring/weighted_average.rb +1 -1
  27. data/lib/decision_agent/version.rb +1 -1
  28. data/lib/decision_agent/versioning/activerecord_adapter.rb +141 -0
  29. data/lib/decision_agent/versioning/adapter.rb +100 -0
  30. data/lib/decision_agent/versioning/file_storage_adapter.rb +290 -0
  31. data/lib/decision_agent/versioning/version_manager.rb +127 -0
  32. data/lib/decision_agent/web/public/app.js +318 -0
  33. data/lib/decision_agent/web/public/index.html +56 -1
  34. data/lib/decision_agent/web/public/styles.css +219 -0
  35. data/lib/decision_agent/web/server.rb +169 -9
  36. data/lib/decision_agent.rb +11 -0
  37. data/lib/generators/decision_agent/install/install_generator.rb +40 -0
  38. data/lib/generators/decision_agent/install/templates/README +47 -0
  39. data/lib/generators/decision_agent/install/templates/migration.rb +37 -0
  40. data/lib/generators/decision_agent/install/templates/rule.rb +30 -0
  41. data/lib/generators/decision_agent/install/templates/rule_version.rb +66 -0
  42. data/spec/activerecord_thread_safety_spec.rb +553 -0
  43. data/spec/agent_spec.rb +13 -13
  44. data/spec/api_contract_spec.rb +16 -16
  45. data/spec/audit_adapters_spec.rb +3 -3
  46. data/spec/comprehensive_edge_cases_spec.rb +86 -86
  47. data/spec/dsl_validation_spec.rb +83 -83
  48. data/spec/edge_cases_spec.rb +23 -23
  49. data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
  50. data/spec/examples.txt +548 -0
  51. data/spec/issue_verification_spec.rb +685 -0
  52. data/spec/json_rule_evaluator_spec.rb +15 -15
  53. data/spec/monitoring/alert_manager_spec.rb +378 -0
  54. data/spec/monitoring/metrics_collector_spec.rb +281 -0
  55. data/spec/monitoring/monitored_agent_spec.rb +222 -0
  56. data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
  57. data/spec/replay_edge_cases_spec.rb +58 -58
  58. data/spec/replay_spec.rb +11 -11
  59. data/spec/rfc8785_canonicalization_spec.rb +215 -0
  60. data/spec/scoring_spec.rb +1 -1
  61. data/spec/spec_helper.rb +9 -0
  62. data/spec/thread_safety_spec.rb +482 -0
  63. data/spec/thread_safety_spec.rb.broken +878 -0
  64. data/spec/versioning_spec.rb +777 -0
  65. data/spec/web_ui_rack_spec.rb +135 -0
  66. metadata +84 -11
@@ -0,0 +1,471 @@
1
+ // Dashboard state
2
+ let ws = null;
3
+ let charts = {};
4
+ let previousStats = null;
5
+ let reconnectInterval = null;
6
+
7
+ // Initialize dashboard on load
8
+ document.addEventListener('DOMContentLoaded', () => {
9
+ initializeCharts();
10
+ connectWebSocket();
11
+ loadInitialData();
12
+
13
+ // Refresh data periodically if WebSocket fails
14
+ setInterval(() => {
15
+ if (!ws || ws.readyState !== WebSocket.OPEN) {
16
+ loadInitialData();
17
+ }
18
+ }, 5000);
19
+ });
20
+
21
+ // WebSocket connection
22
+ function connectWebSocket() {
23
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
24
+ const wsUrl = `${protocol}//${window.location.host}/ws`;
25
+
26
+ ws = new WebSocket(wsUrl);
27
+
28
+ ws.onopen = () => {
29
+ console.log('WebSocket connected');
30
+ updateConnectionStatus(true);
31
+
32
+ // Subscribe to updates
33
+ ws.send(JSON.stringify({ action: 'subscribe' }));
34
+ ws.send(JSON.stringify({ action: 'get_alerts' }));
35
+
36
+ // Clear reconnect interval
37
+ if (reconnectInterval) {
38
+ clearInterval(reconnectInterval);
39
+ reconnectInterval = null;
40
+ }
41
+ };
42
+
43
+ ws.onmessage = (event) => {
44
+ const message = JSON.parse(event.data);
45
+ handleWebSocketMessage(message);
46
+ };
47
+
48
+ ws.onerror = (error) => {
49
+ console.error('WebSocket error:', error);
50
+ updateConnectionStatus(false);
51
+ };
52
+
53
+ ws.onclose = () => {
54
+ console.log('WebSocket disconnected');
55
+ updateConnectionStatus(false);
56
+
57
+ // Attempt reconnect
58
+ if (!reconnectInterval) {
59
+ reconnectInterval = setInterval(() => {
60
+ console.log('Attempting to reconnect...');
61
+ connectWebSocket();
62
+ }, 5000);
63
+ }
64
+ };
65
+ }
66
+
67
+ function handleWebSocketMessage(message) {
68
+ switch (message.type) {
69
+ case 'connected':
70
+ console.log('Connected:', message.message);
71
+ break;
72
+ case 'stats':
73
+ updateDashboard(message.data);
74
+ break;
75
+ case 'metric_update':
76
+ handleMetricUpdate(message.event, message.data);
77
+ break;
78
+ case 'alert':
79
+ handleNewAlert(message.data);
80
+ break;
81
+ case 'alerts':
82
+ updateAlertsTable(message.data);
83
+ break;
84
+ case 'error':
85
+ console.error('Server error:', message.message);
86
+ break;
87
+ }
88
+ }
89
+
90
+ function handleMetricUpdate(eventType, metric) {
91
+ // Real-time metric update - refresh stats
92
+ loadInitialData();
93
+ }
94
+
95
+ function handleNewAlert(alert) {
96
+ showAlertBanner(alert);
97
+ loadAlerts();
98
+ }
99
+
100
+ // Load initial data via API
101
+ async function loadInitialData() {
102
+ try {
103
+ const response = await fetch('/api/stats');
104
+ const stats = await response.json();
105
+ updateDashboard(stats);
106
+ } catch (error) {
107
+ console.error('Failed to load stats:', error);
108
+ }
109
+
110
+ loadAlerts();
111
+ loadHealth();
112
+ }
113
+
114
+ async function loadAlerts() {
115
+ try {
116
+ const response = await fetch('/api/alerts');
117
+ const alerts = await response.json();
118
+ updateAlertsTable(alerts);
119
+ } catch (error) {
120
+ console.error('Failed to load alerts:', error);
121
+ }
122
+ }
123
+
124
+ async function loadHealth() {
125
+ try {
126
+ const response = await fetch('/health');
127
+ const health = await response.json();
128
+
129
+ document.getElementById('agent-version').textContent = health.version;
130
+ document.getElementById('ws-clients').textContent = health.websocket_clients;
131
+
132
+ const totalMetrics = Object.values(health.metrics_count || {}).reduce((a, b) => a + b, 0);
133
+ document.getElementById('metrics-stored').textContent = totalMetrics;
134
+ } catch (error) {
135
+ console.error('Failed to load health:', error);
136
+ }
137
+ }
138
+
139
+ // Update dashboard with stats
140
+ function updateDashboard(stats) {
141
+ updateSummaryCards(stats);
142
+ updateCharts(stats);
143
+
144
+ document.getElementById('last-update').textContent = new Date().toLocaleTimeString();
145
+ previousStats = stats;
146
+ }
147
+
148
+ function updateSummaryCards(stats) {
149
+ // Total Decisions
150
+ const totalDecisions = stats.decisions?.total || 0;
151
+ document.getElementById('total-decisions').textContent = formatNumber(totalDecisions);
152
+
153
+ // Average Confidence
154
+ const avgConfidence = stats.decisions?.avg_confidence || 0;
155
+ document.getElementById('avg-confidence').textContent = avgConfidence.toFixed(2);
156
+
157
+ // Success Rate
158
+ const successRate = stats.performance?.success_rate || 0;
159
+ document.getElementById('success-rate').textContent = `${(successRate * 100).toFixed(1)}%`;
160
+
161
+ // P95 Latency
162
+ const p95Latency = stats.performance?.p95_duration_ms || 0;
163
+ document.getElementById('p95-latency').textContent = `${p95Latency.toFixed(0)}ms`;
164
+
165
+ // Total Errors
166
+ const totalErrors = stats.errors?.total || 0;
167
+ document.getElementById('total-errors').textContent = formatNumber(totalErrors);
168
+
169
+ // Update evaluators count
170
+ const evaluatorsCount = stats.decisions?.evaluators_used?.length || 0;
171
+ document.getElementById('evaluators-count').textContent = evaluatorsCount;
172
+
173
+ // Calculate changes if we have previous stats
174
+ if (previousStats) {
175
+ updateChange('decisions-change', totalDecisions, previousStats.decisions?.total || 0);
176
+ updateChange('confidence-change', avgConfidence, previousStats.decisions?.avg_confidence || 0);
177
+ updateChange('success-change', successRate, previousStats.performance?.success_rate || 0);
178
+ updateChange('latency-change', p95Latency, previousStats.performance?.p95_duration_ms || 0, true);
179
+ updateChange('errors-change', totalErrors, previousStats.errors?.total || 0, true);
180
+ }
181
+ }
182
+
183
+ function updateChange(elementId, current, previous, inverse = false) {
184
+ const element = document.getElementById(elementId);
185
+ const change = current - previous;
186
+
187
+ if (change === 0) {
188
+ element.textContent = 'No change';
189
+ element.className = 'metric-change';
190
+ return;
191
+ }
192
+
193
+ const positive = inverse ? change < 0 : change > 0;
194
+ const sign = change > 0 ? '+' : '';
195
+
196
+ element.textContent = `${sign}${change.toFixed(2)}`;
197
+ element.className = `metric-change ${positive ? 'positive' : 'negative'}`;
198
+ }
199
+
200
+ // Initialize charts
201
+ function initializeCharts() {
202
+ const chartOptions = {
203
+ responsive: true,
204
+ maintainAspectRatio: true,
205
+ plugins: {
206
+ legend: {
207
+ labels: {
208
+ color: '#e6edf3'
209
+ }
210
+ }
211
+ },
212
+ scales: {
213
+ y: {
214
+ ticks: { color: '#8b949e' },
215
+ grid: { color: '#21262d' }
216
+ },
217
+ x: {
218
+ ticks: { color: '#8b949e' },
219
+ grid: { color: '#21262d' }
220
+ }
221
+ }
222
+ };
223
+
224
+ // Throughput chart
225
+ charts.throughput = new Chart(document.getElementById('throughput-chart'), {
226
+ type: 'line',
227
+ data: {
228
+ labels: [],
229
+ datasets: [{
230
+ label: 'Decisions/min',
231
+ data: [],
232
+ borderColor: '#58a6ff',
233
+ backgroundColor: 'rgba(88, 166, 255, 0.1)',
234
+ tension: 0.4
235
+ }]
236
+ },
237
+ options: chartOptions
238
+ });
239
+
240
+ // Distribution chart
241
+ charts.distribution = new Chart(document.getElementById('distribution-chart'), {
242
+ type: 'doughnut',
243
+ data: {
244
+ labels: [],
245
+ datasets: [{
246
+ data: [],
247
+ backgroundColor: ['#58a6ff', '#3fb950', '#d29922', '#da3633', '#bc8cff']
248
+ }]
249
+ },
250
+ options: {
251
+ responsive: true,
252
+ maintainAspectRatio: true,
253
+ plugins: {
254
+ legend: {
255
+ labels: { color: '#e6edf3' }
256
+ }
257
+ }
258
+ }
259
+ });
260
+
261
+ // Performance chart
262
+ charts.performance = new Chart(document.getElementById('performance-chart'), {
263
+ type: 'line',
264
+ data: {
265
+ labels: [],
266
+ datasets: [
267
+ {
268
+ label: 'P95',
269
+ data: [],
270
+ borderColor: '#3fb950',
271
+ backgroundColor: 'rgba(63, 185, 80, 0.1)',
272
+ tension: 0.4
273
+ },
274
+ {
275
+ label: 'P99',
276
+ data: [],
277
+ borderColor: '#d29922',
278
+ backgroundColor: 'rgba(210, 153, 34, 0.1)',
279
+ tension: 0.4
280
+ }
281
+ ]
282
+ },
283
+ options: chartOptions
284
+ });
285
+
286
+ // Error chart
287
+ charts.error = new Chart(document.getElementById('error-chart'), {
288
+ type: 'bar',
289
+ data: {
290
+ labels: [],
291
+ datasets: [{
292
+ label: 'Errors',
293
+ data: [],
294
+ backgroundColor: '#da3633'
295
+ }]
296
+ },
297
+ options: chartOptions
298
+ });
299
+ }
300
+
301
+ function updateCharts(stats) {
302
+ // Update distribution chart
303
+ if (stats.decisions?.decision_distribution) {
304
+ const distribution = stats.decisions.decision_distribution;
305
+ charts.distribution.data.labels = Object.keys(distribution);
306
+ charts.distribution.data.datasets[0].data = Object.values(distribution);
307
+ charts.distribution.update();
308
+ }
309
+
310
+ // For time-series charts, we'd fetch from the API
311
+ updateTimeSeriesCharts();
312
+ }
313
+
314
+ async function updateTimeSeriesCharts() {
315
+ try {
316
+ // Fetch decisions time series
317
+ const decisionsResp = await fetch('/api/timeseries/decisions?bucket_size=60&time_range=3600');
318
+ const decisionsData = await decisionsResp.json();
319
+
320
+ if (decisionsData.length > 0) {
321
+ charts.throughput.data.labels = decisionsData.map(d => new Date(d.timestamp).toLocaleTimeString());
322
+ charts.throughput.data.datasets[0].data = decisionsData.map(d => d.count);
323
+ charts.throughput.update();
324
+ }
325
+
326
+ // Fetch performance time series
327
+ const perfResp = await fetch('/api/timeseries/performance?bucket_size=60&time_range=3600');
328
+ const perfData = await perfResp.json();
329
+
330
+ if (perfData.length > 0) {
331
+ const labels = perfData.map(d => new Date(d.timestamp).toLocaleTimeString());
332
+ const p95Data = perfData.map(d => {
333
+ const durations = d.metrics.map(m => m.duration_ms).sort((a, b) => a - b);
334
+ return durations[Math.floor(durations.length * 0.95)] || 0;
335
+ });
336
+ const p99Data = perfData.map(d => {
337
+ const durations = d.metrics.map(m => m.duration_ms).sort((a, b) => a - b);
338
+ return durations[Math.floor(durations.length * 0.99)] || 0;
339
+ });
340
+
341
+ charts.performance.data.labels = labels;
342
+ charts.performance.data.datasets[0].data = p95Data;
343
+ charts.performance.data.datasets[1].data = p99Data;
344
+ charts.performance.update();
345
+ }
346
+
347
+ // Fetch error time series
348
+ const errorsResp = await fetch('/api/timeseries/errors?bucket_size=60&time_range=3600');
349
+ const errorsData = await errorsResp.json();
350
+
351
+ if (errorsData.length > 0) {
352
+ charts.error.data.labels = errorsData.map(d => new Date(d.timestamp).toLocaleTimeString());
353
+ charts.error.data.datasets[0].data = errorsData.map(d => d.count);
354
+ charts.error.update();
355
+ }
356
+ } catch (error) {
357
+ console.error('Failed to update time series charts:', error);
358
+ }
359
+ }
360
+
361
+ // Update alerts table
362
+ function updateAlertsTable(alerts) {
363
+ const tbody = document.getElementById('alerts-tbody');
364
+ document.getElementById('active-alerts').textContent = alerts.length;
365
+
366
+ if (alerts.length === 0) {
367
+ tbody.innerHTML = '<tr><td colspan="5" class="no-data">No active alerts</td></tr>';
368
+ return;
369
+ }
370
+
371
+ tbody.innerHTML = alerts.map(alert => `
372
+ <tr>
373
+ <td><span class="severity-badge severity-${alert.severity}">${alert.severity.toUpperCase()}</span></td>
374
+ <td>${alert.rule_name}</td>
375
+ <td>${alert.message}</td>
376
+ <td>${new Date(alert.triggered_at).toLocaleString()}</td>
377
+ <td class="alert-actions">
378
+ <button onclick="acknowledgeAlert('${alert.id}')">Acknowledge</button>
379
+ <button onclick="resolveAlert('${alert.id}')">Resolve</button>
380
+ </td>
381
+ </tr>
382
+ `).join('');
383
+ }
384
+
385
+ // Alert actions
386
+ async function acknowledgeAlert(alertId) {
387
+ try {
388
+ await fetch(`/api/alerts/${alertId}/acknowledge`, {
389
+ method: 'POST',
390
+ headers: { 'Content-Type': 'application/json' },
391
+ body: JSON.stringify({ acknowledged_by: 'dashboard_user' })
392
+ });
393
+ loadAlerts();
394
+ } catch (error) {
395
+ console.error('Failed to acknowledge alert:', error);
396
+ }
397
+ }
398
+
399
+ async function resolveAlert(alertId) {
400
+ try {
401
+ await fetch(`/api/alerts/${alertId}/resolve`, {
402
+ method: 'POST',
403
+ headers: { 'Content-Type': 'application/json' },
404
+ body: JSON.stringify({ resolved_by: 'dashboard_user' })
405
+ });
406
+ loadAlerts();
407
+ } catch (error) {
408
+ console.error('Failed to resolve alert:', error);
409
+ }
410
+ }
411
+
412
+ // KPI registration
413
+ async function registerKPI(event) {
414
+ event.preventDefault();
415
+
416
+ const name = document.getElementById('kpi-name').value;
417
+ const value = parseFloat(document.getElementById('kpi-value').value);
418
+
419
+ try {
420
+ await fetch('/api/kpi', {
421
+ method: 'POST',
422
+ headers: { 'Content-Type': 'application/json' },
423
+ body: JSON.stringify({ name, value })
424
+ });
425
+
426
+ document.getElementById('kpi-form').reset();
427
+ showAlertBanner({ message: `KPI "${name}" registered successfully`, severity: 'info' });
428
+ } catch (error) {
429
+ console.error('Failed to register KPI:', error);
430
+ showAlertBanner({ message: 'Failed to register KPI', severity: 'critical' });
431
+ }
432
+ }
433
+
434
+ // UI helpers
435
+ function updateConnectionStatus(connected) {
436
+ const statusElement = document.getElementById('connection-status');
437
+ if (connected) {
438
+ statusElement.textContent = 'Connected';
439
+ statusElement.className = 'status-badge connected';
440
+ } else {
441
+ statusElement.textContent = 'Disconnected';
442
+ statusElement.className = 'status-badge disconnected';
443
+ }
444
+ }
445
+
446
+ function showAlertBanner(alert) {
447
+ const banner = document.getElementById('alert-bar');
448
+ const message = document.getElementById('alert-message');
449
+
450
+ message.textContent = alert.message;
451
+ banner.style.display = 'block';
452
+ banner.className = `alert-bar severity-${alert.severity || 'warning'}`;
453
+
454
+ setTimeout(() => {
455
+ banner.style.display = 'none';
456
+ }, 5000);
457
+ }
458
+
459
+ function closeAlertBar() {
460
+ document.getElementById('alert-bar').style.display = 'none';
461
+ }
462
+
463
+ function formatNumber(num) {
464
+ if (num >= 1000000) return (num / 1000000).toFixed(1) + 'M';
465
+ if (num >= 1000) return (num / 1000).toFixed(1) + 'K';
466
+ return num.toString();
467
+ }
468
+
469
+ async function refreshAlerts() {
470
+ await loadAlerts();
471
+ }
@@ -0,0 +1,161 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>DecisionAgent Monitoring Dashboard</title>
7
+ <link rel="stylesheet" href="dashboard.css">
8
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
9
+ </head>
10
+ <body>
11
+ <div class="dashboard">
12
+ <header class="header">
13
+ <h1>DecisionAgent Monitoring</h1>
14
+ <div class="header-info">
15
+ <span id="connection-status" class="status-badge disconnected">Disconnected</span>
16
+ <span id="last-update">Never</span>
17
+ </div>
18
+ </header>
19
+
20
+ <div class="alert-bar" id="alert-bar" style="display: none;">
21
+ <div class="alert-content">
22
+ <span class="alert-icon">⚠️</span>
23
+ <span id="alert-message"></span>
24
+ <button class="alert-close" onclick="closeAlertBar()">×</button>
25
+ </div>
26
+ </div>
27
+
28
+ <div class="metrics-grid">
29
+ <!-- Summary Cards -->
30
+ <div class="metric-card">
31
+ <div class="metric-label">Total Decisions</div>
32
+ <div class="metric-value" id="total-decisions">0</div>
33
+ <div class="metric-change" id="decisions-change">--</div>
34
+ </div>
35
+
36
+ <div class="metric-card">
37
+ <div class="metric-label">Avg Confidence</div>
38
+ <div class="metric-value" id="avg-confidence">0.00</div>
39
+ <div class="metric-change" id="confidence-change">--</div>
40
+ </div>
41
+
42
+ <div class="metric-card">
43
+ <div class="metric-label">Success Rate</div>
44
+ <div class="metric-value" id="success-rate">0%</div>
45
+ <div class="metric-change" id="success-change">--</div>
46
+ </div>
47
+
48
+ <div class="metric-card">
49
+ <div class="metric-label">P95 Latency</div>
50
+ <div class="metric-value" id="p95-latency">0ms</div>
51
+ <div class="metric-change" id="latency-change">--</div>
52
+ </div>
53
+
54
+ <div class="metric-card error-card">
55
+ <div class="metric-label">Total Errors</div>
56
+ <div class="metric-value" id="total-errors">0</div>
57
+ <div class="metric-change" id="errors-change">--</div>
58
+ </div>
59
+
60
+ <div class="metric-card">
61
+ <div class="metric-label">Active Alerts</div>
62
+ <div class="metric-value" id="active-alerts">0</div>
63
+ <div class="metric-change" id="alerts-change">--</div>
64
+ </div>
65
+ </div>
66
+
67
+ <div class="charts-grid">
68
+ <!-- Decision Throughput Chart -->
69
+ <div class="chart-card">
70
+ <h3>Decision Throughput</h3>
71
+ <canvas id="throughput-chart"></canvas>
72
+ </div>
73
+
74
+ <!-- Confidence Distribution -->
75
+ <div class="chart-card">
76
+ <h3>Decision Distribution</h3>
77
+ <canvas id="distribution-chart"></canvas>
78
+ </div>
79
+
80
+ <!-- Performance Metrics -->
81
+ <div class="chart-card">
82
+ <h3>Performance (P95/P99 Latency)</h3>
83
+ <canvas id="performance-chart"></canvas>
84
+ </div>
85
+
86
+ <!-- Error Rate -->
87
+ <div class="chart-card">
88
+ <h3>Error Rate Over Time</h3>
89
+ <canvas id="error-chart"></canvas>
90
+ </div>
91
+ </div>
92
+
93
+ <div class="data-grid">
94
+ <!-- Alerts Table -->
95
+ <div class="data-card">
96
+ <div class="data-header">
97
+ <h3>Active Alerts</h3>
98
+ <button class="btn-small" onclick="refreshAlerts()">Refresh</button>
99
+ </div>
100
+ <div class="table-container">
101
+ <table id="alerts-table">
102
+ <thead>
103
+ <tr>
104
+ <th>Severity</th>
105
+ <th>Rule</th>
106
+ <th>Message</th>
107
+ <th>Triggered</th>
108
+ <th>Actions</th>
109
+ </tr>
110
+ </thead>
111
+ <tbody id="alerts-tbody">
112
+ <tr>
113
+ <td colspan="5" class="no-data">No active alerts</td>
114
+ </tr>
115
+ </tbody>
116
+ </table>
117
+ </div>
118
+ </div>
119
+
120
+ <!-- Recent Decisions -->
121
+ <div class="data-card">
122
+ <div class="data-header">
123
+ <h3>System Metrics</h3>
124
+ </div>
125
+ <div class="system-metrics" id="system-metrics">
126
+ <div class="system-metric">
127
+ <span class="metric-name">Evaluators Active:</span>
128
+ <span class="metric-val" id="evaluators-count">0</span>
129
+ </div>
130
+ <div class="system-metric">
131
+ <span class="metric-name">Metrics Stored:</span>
132
+ <span class="metric-val" id="metrics-stored">0</span>
133
+ </div>
134
+ <div class="system-metric">
135
+ <span class="metric-name">Version:</span>
136
+ <span class="metric-val" id="agent-version">--</span>
137
+ </div>
138
+ <div class="system-metric">
139
+ <span class="metric-name">WebSocket Clients:</span>
140
+ <span class="metric-val" id="ws-clients">0</span>
141
+ </div>
142
+ </div>
143
+ </div>
144
+ </div>
145
+
146
+ <!-- Custom KPI Form -->
147
+ <div class="kpi-section">
148
+ <h3>Register Custom KPI</h3>
149
+ <form id="kpi-form" onsubmit="registerKPI(event)">
150
+ <div class="form-group">
151
+ <input type="text" id="kpi-name" placeholder="KPI Name" required>
152
+ <input type="number" id="kpi-value" placeholder="Value" step="0.01" required>
153
+ <button type="submit" class="btn-primary">Register KPI</button>
154
+ </div>
155
+ </form>
156
+ </div>
157
+ </div>
158
+
159
+ <script src="dashboard.js"></script>
160
+ </body>
161
+ </html>