decision_agent 0.1.2 → 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.
- checksums.yaml +4 -4
- data/README.md +212 -35
- data/bin/decision_agent +3 -8
- data/lib/decision_agent/agent.rb +19 -26
- data/lib/decision_agent/audit/null_adapter.rb +1 -2
- data/lib/decision_agent/decision.rb +3 -1
- data/lib/decision_agent/dsl/condition_evaluator.rb +4 -3
- data/lib/decision_agent/dsl/rule_parser.rb +4 -6
- data/lib/decision_agent/dsl/schema_validator.rb +27 -31
- data/lib/decision_agent/errors.rb +11 -8
- data/lib/decision_agent/evaluation.rb +3 -1
- data/lib/decision_agent/evaluation_validator.rb +78 -0
- data/lib/decision_agent/evaluators/json_rule_evaluator.rb +26 -0
- data/lib/decision_agent/evaluators/static_evaluator.rb +2 -6
- data/lib/decision_agent/monitoring/alert_manager.rb +282 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.css +381 -0
- data/lib/decision_agent/monitoring/dashboard/public/dashboard.js +471 -0
- data/lib/decision_agent/monitoring/dashboard/public/index.html +161 -0
- data/lib/decision_agent/monitoring/dashboard_server.rb +340 -0
- data/lib/decision_agent/monitoring/metrics_collector.rb +278 -0
- data/lib/decision_agent/monitoring/monitored_agent.rb +71 -0
- data/lib/decision_agent/monitoring/prometheus_exporter.rb +247 -0
- data/lib/decision_agent/replay/replay.rb +12 -22
- data/lib/decision_agent/scoring/base.rb +1 -1
- data/lib/decision_agent/scoring/consensus.rb +5 -5
- data/lib/decision_agent/scoring/weighted_average.rb +1 -1
- data/lib/decision_agent/version.rb +1 -1
- data/lib/decision_agent/versioning/activerecord_adapter.rb +69 -33
- data/lib/decision_agent/versioning/adapter.rb +1 -3
- data/lib/decision_agent/versioning/file_storage_adapter.rb +143 -35
- data/lib/decision_agent/versioning/version_manager.rb +4 -12
- data/lib/decision_agent/web/public/index.html +1 -1
- data/lib/decision_agent/web/server.rb +19 -24
- data/lib/decision_agent.rb +7 -0
- data/lib/generators/decision_agent/install/install_generator.rb +5 -5
- data/lib/generators/decision_agent/install/templates/migration.rb +17 -6
- data/lib/generators/decision_agent/install/templates/rule.rb +3 -3
- data/lib/generators/decision_agent/install/templates/rule_version.rb +13 -7
- data/spec/activerecord_thread_safety_spec.rb +553 -0
- data/spec/agent_spec.rb +13 -13
- data/spec/api_contract_spec.rb +16 -16
- data/spec/audit_adapters_spec.rb +3 -3
- data/spec/comprehensive_edge_cases_spec.rb +86 -86
- data/spec/dsl_validation_spec.rb +83 -83
- data/spec/edge_cases_spec.rb +23 -23
- data/spec/examples/feedback_aware_evaluator_spec.rb +7 -7
- data/spec/examples.txt +548 -0
- data/spec/issue_verification_spec.rb +685 -0
- data/spec/json_rule_evaluator_spec.rb +15 -15
- data/spec/monitoring/alert_manager_spec.rb +378 -0
- data/spec/monitoring/metrics_collector_spec.rb +281 -0
- data/spec/monitoring/monitored_agent_spec.rb +222 -0
- data/spec/monitoring/prometheus_exporter_spec.rb +242 -0
- data/spec/replay_edge_cases_spec.rb +58 -58
- data/spec/replay_spec.rb +11 -11
- data/spec/rfc8785_canonicalization_spec.rb +215 -0
- data/spec/scoring_spec.rb +1 -1
- data/spec/spec_helper.rb +9 -0
- data/spec/thread_safety_spec.rb +482 -0
- data/spec/thread_safety_spec.rb.broken +878 -0
- data/spec/versioning_spec.rb +141 -37
- data/spec/web_ui_rack_spec.rb +135 -0
- metadata +69 -6
|
@@ -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>
|