agentflow-dashboard 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,166 +1,177 @@
1
- class AgentFlowDashboard {
2
- constructor() {
3
- this.ws = null;
4
- this.reconnectAttempts = 0;
5
- this.maxReconnectAttempts = 10;
6
- this.reconnectDelay = 1000;
7
- this.selectedAgent = null;
8
- this.traces = [];
9
- this.agents = [];
10
- this.stats = null;
11
-
12
- this.init();
13
- }
14
-
15
- init() {
16
- this.connectWebSocket();
17
- this.setupEventListeners();
18
- this.loadInitialData();
19
- }
1
+ function escapeHtml(str) {
2
+ if (typeof str !== 'string') return str == null ? '' : String(str);
3
+ return str
4
+ .replace(/&/g, '&')
5
+ .replace(/</g, '&lt;')
6
+ .replace(/>/g, '&gt;')
7
+ .replace(/"/g, '&quot;')
8
+ .replace(/'/g, '&#39;');
9
+ }
20
10
 
21
- connectWebSocket() {
22
- const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
23
- const wsUrl = `${protocol}//${window.location.host}`;
24
-
25
- this.ws = new WebSocket(wsUrl);
26
-
27
- this.ws.onopen = () => {
28
- console.log('Connected to AgentFlow Dashboard');
29
- this.reconnectAttempts = 0;
30
- this.updateConnectionStatus(true);
31
- };
32
-
33
- this.ws.onmessage = (event) => {
34
- try {
35
- const message = JSON.parse(event.data);
36
- this.handleWebSocketMessage(message);
37
- } catch (error) {
38
- console.error('Error parsing WebSocket message:', error);
39
- }
40
- };
41
-
42
- this.ws.onclose = () => {
43
- console.log('Disconnected from AgentFlow Dashboard');
44
- this.updateConnectionStatus(false);
45
- this.attemptReconnect();
46
- };
47
-
48
- this.ws.onerror = (error) => {
49
- console.error('WebSocket error:', error);
50
- this.updateConnectionStatus(false);
51
- };
11
+ class AgentFlowDashboard {
12
+ constructor() {
13
+ this.ws = null;
14
+ this.reconnectAttempts = 0;
15
+ this.maxReconnectAttempts = 10;
16
+ this.reconnectDelay = 1000;
17
+ this.selectedAgent = null;
18
+ this.traces = [];
19
+ this.agents = [];
20
+ this.stats = null;
21
+ this.processHealth = null;
22
+
23
+ this.init();
24
+ }
25
+
26
+ init() {
27
+ this.connectWebSocket();
28
+ this.setupEventListeners();
29
+ this.loadInitialData();
30
+ this.loadProcessHealth();
31
+ this._processHealthInterval = setInterval(() => this.loadProcessHealth(), 10000);
32
+ }
33
+
34
+ connectWebSocket() {
35
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
36
+ const wsUrl = `${protocol}//${window.location.host}`;
37
+
38
+ this.ws = new WebSocket(wsUrl);
39
+
40
+ this.ws.onopen = () => {
41
+ console.log('Connected to AgentFlow Dashboard');
42
+ this.reconnectAttempts = 0;
43
+ this.updateConnectionStatus(true);
44
+ };
45
+
46
+ this.ws.onmessage = (event) => {
47
+ try {
48
+ const message = JSON.parse(event.data);
49
+ this.handleWebSocketMessage(message);
50
+ } catch (error) {
51
+ console.error('Error parsing WebSocket message:', error);
52
+ }
53
+ };
54
+
55
+ this.ws.onclose = () => {
56
+ console.log('Disconnected from AgentFlow Dashboard');
57
+ this.updateConnectionStatus(false);
58
+ this.attemptReconnect();
59
+ };
60
+
61
+ this.ws.onerror = (error) => {
62
+ console.error('WebSocket error:', error);
63
+ this.updateConnectionStatus(false);
64
+ };
65
+ }
66
+
67
+ attemptReconnect() {
68
+ if (this.reconnectAttempts >= this.maxReconnectAttempts) {
69
+ console.log('Max reconnection attempts reached');
70
+ return;
52
71
  }
53
72
 
54
- attemptReconnect() {
55
- if (this.reconnectAttempts >= this.maxReconnectAttempts) {
56
- console.log('Max reconnection attempts reached');
57
- return;
73
+ this.reconnectAttempts++;
74
+ const delay = this.reconnectDelay * 1.5 ** (this.reconnectAttempts - 1);
75
+
76
+ setTimeout(() => {
77
+ console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
78
+ this.connectWebSocket();
79
+ }, delay);
80
+ }
81
+
82
+ handleWebSocketMessage(message) {
83
+ switch (message.type) {
84
+ case 'init':
85
+ this.traces = message.data.traces || [];
86
+ this.stats = message.data.stats || null;
87
+ this.updateUI();
88
+ break;
89
+
90
+ case 'trace-added':
91
+ this.traces.unshift(message.data);
92
+ this.updateTraces();
93
+ this.refreshStats();
94
+ break;
95
+
96
+ case 'trace-updated': {
97
+ const index = this.traces.findIndex((t) => t.filename === message.data.filename);
98
+ if (index >= 0) {
99
+ this.traces[index] = message.data;
100
+ this.updateTraces();
58
101
  }
102
+ break;
103
+ }
59
104
 
60
- this.reconnectAttempts++;
61
- const delay = this.reconnectDelay * Math.pow(1.5, this.reconnectAttempts - 1);
62
-
63
- setTimeout(() => {
64
- console.log(`Reconnection attempt ${this.reconnectAttempts}/${this.maxReconnectAttempts}`);
65
- this.connectWebSocket();
66
- }, delay);
67
- }
105
+ case 'stats-updated':
106
+ this.stats = message.data;
107
+ this.updateStats();
108
+ this.updateAgents();
109
+ break;
68
110
 
69
- handleWebSocketMessage(message) {
70
- switch (message.type) {
71
- case 'init':
72
- this.traces = message.data.traces || [];
73
- this.stats = message.data.stats || null;
74
- this.updateUI();
75
- break;
76
-
77
- case 'trace-added':
78
- this.traces.unshift(message.data);
79
- this.updateTraces();
80
- this.refreshStats();
81
- break;
82
-
83
- case 'trace-updated':
84
- const index = this.traces.findIndex(t => t.filename === message.data.filename);
85
- if (index >= 0) {
86
- this.traces[index] = message.data;
87
- this.updateTraces();
88
- }
89
- break;
90
-
91
- case 'stats-updated':
92
- this.stats = message.data;
93
- this.updateStats();
94
- this.updateAgents();
95
- break;
96
-
97
- default:
98
- console.log('Unknown message type:', message.type);
99
- }
111
+ default:
112
+ console.log('Unknown message type:', message.type);
100
113
  }
114
+ }
101
115
 
102
- async loadInitialData() {
103
- try {
104
- const [tracesRes, statsRes] = await Promise.all([
105
- fetch('/api/traces'),
106
- fetch('/api/stats')
107
- ]);
116
+ async loadInitialData() {
117
+ try {
118
+ const [tracesRes, statsRes] = await Promise.all([fetch('/api/traces'), fetch('/api/stats')]);
108
119
 
109
- if (tracesRes.ok) {
110
- this.traces = await tracesRes.json();
111
- }
120
+ if (tracesRes.ok) {
121
+ this.traces = await tracesRes.json();
122
+ }
112
123
 
113
- if (statsRes.ok) {
114
- this.stats = await statsRes.json();
115
- }
124
+ if (statsRes.ok) {
125
+ this.stats = await statsRes.json();
126
+ }
116
127
 
117
- this.updateUI();
118
- } catch (error) {
119
- console.error('Error loading initial data:', error);
120
- }
128
+ this.updateUI();
129
+ } catch (error) {
130
+ console.error('Error loading initial data:', error);
121
131
  }
132
+ }
122
133
 
123
- async refreshStats() {
124
- try {
125
- const response = await fetch('/api/stats');
126
- if (response.ok) {
127
- this.stats = await response.json();
128
- this.updateStats();
129
- this.updateAgents();
130
- }
131
- } catch (error) {
132
- console.error('Error refreshing stats:', error);
133
- }
134
- }
135
-
136
- updateUI() {
134
+ async refreshStats() {
135
+ try {
136
+ const response = await fetch('/api/stats');
137
+ if (response.ok) {
138
+ this.stats = await response.json();
137
139
  this.updateStats();
138
140
  this.updateAgents();
139
- this.updateTraces();
141
+ }
142
+ } catch (error) {
143
+ console.error('Error refreshing stats:', error);
140
144
  }
141
-
142
- updateConnectionStatus(connected) {
143
- const statusEl = document.getElementById('connectionStatus');
144
- if (connected) {
145
- statusEl.textContent = 'Connected';
146
- statusEl.className = 'connection-status connected';
147
- } else {
148
- statusEl.textContent = 'Disconnected';
149
- statusEl.className = 'connection-status disconnected';
150
- }
145
+ }
146
+
147
+ updateUI() {
148
+ this.updateStats();
149
+ this.updateAgents();
150
+ this.updateTraces();
151
+ }
152
+
153
+ updateConnectionStatus(connected) {
154
+ const statusEl = document.getElementById('connectionStatus');
155
+ if (connected) {
156
+ statusEl.textContent = 'Connected';
157
+ statusEl.className = 'connection-status connected';
158
+ } else {
159
+ statusEl.textContent = 'Disconnected';
160
+ statusEl.className = 'connection-status disconnected';
151
161
  }
162
+ }
152
163
 
153
- updateStats() {
154
- const statsGrid = document.getElementById('statsGrid');
164
+ updateStats() {
165
+ const statsGrid = document.getElementById('statsGrid');
155
166
 
156
- if (!this.stats) {
157
- statsGrid.innerHTML = '<div class="loading">Loading statistics...</div>';
158
- return;
159
- }
167
+ if (!this.stats) {
168
+ statsGrid.innerHTML = '<div class="loading">Loading statistics...</div>';
169
+ return;
170
+ }
160
171
 
161
- const successRate = Math.round(this.stats.globalSuccessRate * 10) / 10;
172
+ const successRate = Math.round(this.stats.globalSuccessRate * 10) / 10;
162
173
 
163
- statsGrid.innerHTML = `
174
+ statsGrid.innerHTML = `
164
175
  <div class="stat-card">
165
176
  <h3>Total Agents</h3>
166
177
  <div class="value">${this.stats.totalAgents}</div>
@@ -178,23 +189,24 @@ class AgentFlowDashboard {
178
189
  <div class="value">${this.stats.activeAgents}</div>
179
190
  </div>
180
191
  `;
181
- }
192
+ }
182
193
 
183
- updateAgents() {
184
- const agentList = document.getElementById('agentList');
194
+ updateAgents() {
195
+ const agentList = document.getElementById('agentList');
185
196
 
186
- if (!this.stats || !this.stats.topAgents) {
187
- agentList.innerHTML = '<div class="loading">Loading agents...</div>';
188
- return;
189
- }
197
+ if (!this.stats || !this.stats.topAgents) {
198
+ agentList.innerHTML = '<div class="loading">Loading agents...</div>';
199
+ return;
200
+ }
190
201
 
191
- const agentItems = this.stats.topAgents.map(agent => {
192
- const successRate = Math.round(agent.successRate * 10) / 10;
193
- let rateClass = 'success-rate';
194
- if (successRate < 50) rateClass += ' critical';
195
- else if (successRate < 80) rateClass += ' low';
202
+ const agentItems = this.stats.topAgents
203
+ .map((agent) => {
204
+ const successRate = Math.round(agent.successRate * 10) / 10;
205
+ let rateClass = 'success-rate';
206
+ if (successRate < 50) rateClass += ' critical';
207
+ else if (successRate < 80) rateClass += ' low';
196
208
 
197
- return `
209
+ return `
198
210
  <div class="agent-item" data-agent-id="${agent.agentId}">
199
211
  <div class="agent-name">${agent.agentId}</div>
200
212
  <div class="agent-stats">
@@ -203,140 +215,240 @@ class AgentFlowDashboard {
203
215
  </div>
204
216
  </div>
205
217
  `;
206
- }).join('');
218
+ })
219
+ .join('');
207
220
 
208
- agentList.innerHTML = agentItems;
209
- }
221
+ agentList.innerHTML = agentItems;
222
+ }
210
223
 
211
- updateTraces() {
212
- const tracesList = document.getElementById('tracesList');
224
+ updateTraces() {
225
+ const tracesList = document.getElementById('tracesList');
213
226
 
214
- if (!this.traces || this.traces.length === 0) {
215
- tracesList.innerHTML = '<div class="loading">No traces available</div>';
216
- return;
217
- }
227
+ if (!this.traces || this.traces.length === 0) {
228
+ tracesList.innerHTML = '<div class="loading">No traces available</div>';
229
+ return;
230
+ }
218
231
 
219
- // Filter traces if an agent is selected
220
- const filteredTraces = this.selectedAgent ?
221
- this.traces.filter(trace => trace.agentId === this.selectedAgent) :
222
- this.traces;
232
+ // Filter traces if an agent is selected
233
+ const filteredTraces = this.selectedAgent
234
+ ? this.traces.filter((trace) => trace.agentId === this.selectedAgent)
235
+ : this.traces;
223
236
 
224
- const traceItems = filteredTraces.slice(0, 50).map(trace => {
225
- const timestamp = new Date(trace.timestamp).toLocaleString();
226
- const statusClass = this.getTraceStatusClass(trace);
237
+ const traceItems = filteredTraces
238
+ .slice(0, 50)
239
+ .map((trace) => {
240
+ const timestamp = new Date(trace.timestamp).toLocaleString();
241
+ const statusClass = this.getTraceStatusClass(trace);
227
242
 
228
- return `
243
+ return `
229
244
  <div class="trace-item">
230
245
  <div class="trace-header">
231
246
  <div class="trace-name">
232
247
  <span class="status-indicator ${statusClass}"></span>
233
- ${trace.name || `${trace.agentId} execution`}
248
+ ${escapeHtml(trace.name) || `${escapeHtml(trace.agentId)} execution`}
234
249
  </div>
235
- <div class="trace-timestamp">${timestamp}</div>
250
+ <div class="trace-timestamp">${escapeHtml(timestamp)}</div>
236
251
  </div>
237
252
  <div class="trace-details">
238
- <div class="trace-agent">${trace.agentId}</div>
239
- <div class="trace-trigger">${trace.trigger}</div>
253
+ <div class="trace-agent">${escapeHtml(trace.agentId)}</div>
254
+ <div class="trace-trigger">${escapeHtml(trace.trigger)}</div>
240
255
  </div>
241
256
  </div>
242
257
  `;
243
- }).join('');
244
-
245
- tracesList.innerHTML = traceItems || '<div class="loading">No traces found</div>';
258
+ })
259
+ .join('');
260
+
261
+ tracesList.innerHTML = traceItems || '<div class="loading">No traces found</div>';
262
+ }
263
+
264
+ getTraceStatusClass(trace) {
265
+ // Try to determine status from the trace structure
266
+ if (trace.nodes) {
267
+ const nodes = Array.isArray(trace.nodes)
268
+ ? trace.nodes.map(([, node]) => node)
269
+ : trace.nodes instanceof Map
270
+ ? Array.from(trace.nodes.values())
271
+ : Object.values(trace.nodes);
272
+
273
+ const hasFailures = nodes.some(
274
+ (node) => node.status === 'failed' || node.error || (node.metadata && node.metadata.error),
275
+ );
276
+
277
+ if (hasFailures) return 'status-failure';
278
+
279
+ const hasCompleted = nodes.some(
280
+ (node) =>
281
+ node.status === 'completed' ||
282
+ node.endTime ||
283
+ (node.metadata && node.metadata.status === 'success'),
284
+ );
285
+
286
+ if (hasCompleted) return 'status-success';
246
287
  }
247
288
 
248
- getTraceStatusClass(trace) {
249
- // Try to determine status from the trace structure
250
- if (trace.nodes) {
251
- const nodes = Array.isArray(trace.nodes) ?
252
- trace.nodes.map(([, node]) => node) :
253
- trace.nodes instanceof Map ?
254
- Array.from(trace.nodes.values()) :
255
- Object.values(trace.nodes);
256
-
257
- const hasFailures = nodes.some(node =>
258
- node.status === 'failed' ||
259
- node.error ||
260
- (node.metadata && node.metadata.error)
261
- );
262
-
263
- if (hasFailures) return 'status-failure';
264
-
265
- const hasCompleted = nodes.some(node =>
266
- node.status === 'completed' ||
267
- node.endTime ||
268
- (node.metadata && node.metadata.status === 'success')
269
- );
270
-
271
- if (hasCompleted) return 'status-success';
272
- }
289
+ return 'status-unknown';
290
+ }
291
+
292
+ setupEventListeners() {
293
+ // Agent selection
294
+ document.addEventListener('click', (event) => {
295
+ const agentItem = event.target.closest('.agent-item');
296
+ if (agentItem) {
297
+ const agentId = agentItem.dataset.agentId;
298
+ this.selectAgent(agentId);
299
+ }
300
+ });
301
+
302
+ // Auto-refresh every 30 seconds
303
+ setInterval(() => {
304
+ if (this.ws && this.ws.readyState === WebSocket.OPEN) {
305
+ this.refreshStats();
306
+ }
307
+ }, 30000);
308
+ }
309
+
310
+ selectAgent(agentId) {
311
+ // Update UI selection
312
+ document.querySelectorAll('.agent-item').forEach((item) => {
313
+ item.classList.remove('active');
314
+ });
315
+
316
+ const selectedItem = document.querySelector(`[data-agent-id="${agentId}"]`);
317
+ if (selectedItem) {
318
+ selectedItem.classList.add('active');
319
+ this.selectedAgent = agentId;
320
+ } else {
321
+ this.selectedAgent = null;
322
+ }
273
323
 
274
- return 'status-unknown';
324
+ // Update traces view
325
+ this.updateTraces();
326
+
327
+ // Update page title
328
+ document.title = this.selectedAgent
329
+ ? `AgentFlow Dashboard - ${this.selectedAgent}`
330
+ : 'AgentFlow Dashboard';
331
+ }
332
+
333
+ async loadProcessHealth() {
334
+ try {
335
+ const res = await fetch('/api/process-health');
336
+ if (!res.ok) return;
337
+ this.processHealth = await res.json();
338
+ this.renderProcessHealth();
339
+ } catch (error) {
340
+ console.error('Error loading process health:', error);
275
341
  }
342
+ }
276
343
 
277
- setupEventListeners() {
278
- // Agent selection
279
- document.addEventListener('click', (event) => {
280
- const agentItem = event.target.closest('.agent-item');
281
- if (agentItem) {
282
- const agentId = agentItem.dataset.agentId;
283
- this.selectAgent(agentId);
284
- }
285
- });
286
-
287
- // Auto-refresh every 30 seconds
288
- setInterval(() => {
289
- if (this.ws && this.ws.readyState === WebSocket.OPEN) {
290
- this.refreshStats();
291
- }
292
- }, 30000);
344
+ renderProcessHealth() {
345
+ const container = document.getElementById('processHealth');
346
+ if (!this.processHealth) {
347
+ container.style.display = 'none';
348
+ return;
293
349
  }
294
350
 
295
- selectAgent(agentId) {
296
- // Update UI selection
297
- document.querySelectorAll('.agent-item').forEach(item => {
298
- item.classList.remove('active');
299
- });
300
-
301
- const selectedItem = document.querySelector(`[data-agent-id="${agentId}"]`);
302
- if (selectedItem) {
303
- selectedItem.classList.add('active');
304
- this.selectedAgent = agentId;
305
- } else {
306
- this.selectedAgent = null;
307
- }
351
+ container.style.display = '';
352
+ const r = this.processHealth;
353
+ let html = '<h3 class="section-title">Process Health</h3>';
354
+
355
+ // Main status card
356
+ html += '<div class="process-health-card"><h4>Process Status</h4>';
357
+
358
+ // PID file info
359
+ if (r.pidFile) {
360
+ const pf = r.pidFile;
361
+ const cls = pf.alive && pf.matchesProcess ? 'ok' : pf.stale ? 'bad' : 'warn';
362
+ html += '<div class="ph-row">';
363
+ html += '<span class="ph-label">PID File</span>';
364
+ html += '<span class="ph-value ' + cls + '">';
365
+ html += pf.pid ? ('PID ' + pf.pid + (pf.alive ? ' (alive)' : ' (dead)')) : 'No PID';
366
+ html += '</span>';
367
+ html += '</div>';
368
+ }
308
369
 
309
- // Update traces view
310
- this.updateTraces();
370
+ // Systemd info
371
+ if (r.systemd) {
372
+ const sd = r.systemd;
373
+ const cls = sd.activeState === 'active' ? 'ok' : sd.failed ? 'bad' : 'warn';
374
+ html += '<div class="ph-row">';
375
+ html += '<span class="ph-label">Systemd</span>';
376
+ html += '<span class="ph-value ' + cls + '">';
377
+ html += escapeHtml(sd.unit) + ' \u2014 ' + escapeHtml(sd.activeState) + ' (' + escapeHtml(sd.subState) + ')';
378
+ if (sd.restarts > 0) html += ' [' + sd.restarts + ' restarts]';
379
+ html += '</span>';
380
+ html += '</div>';
381
+ }
311
382
 
312
- // Update page title
313
- document.title = this.selectedAgent ?
314
- `AgentFlow Dashboard - ${this.selectedAgent}` :
315
- 'AgentFlow Dashboard';
383
+ // Workers as dots
384
+ if (r.workers) {
385
+ const w = r.workers;
386
+ html += '<div class="ph-row">';
387
+ html += '<span class="ph-label">Workers</span>';
388
+ html += '<div class="worker-dots">';
389
+ for (const worker of w.workers) {
390
+ const dotCls = worker.alive ? 'alive' : worker.stale ? 'stale' : 'unknown';
391
+ html += '<span class="worker-dot ' + dotCls + '" title="' + escapeHtml(worker.name) + ' (pid ' + (worker.pid || '-') + ') \u2014 ' + escapeHtml(worker.declaredStatus) + '"></span>';
392
+ html += '<span class="worker-dot-label">' + escapeHtml(worker.name) + '</span>';
393
+ }
394
+ html += '</div></div>';
316
395
  }
317
396
 
318
- // Public methods for debugging
319
- getStats() {
320
- return this.stats;
397
+ // Problems
398
+ if (r.problems && r.problems.length > 0) {
399
+ html += '<ul class="problems-list">';
400
+ for (const p of r.problems) {
401
+ html += '<li>' + escapeHtml(p) + '</li>';
402
+ }
403
+ html += '</ul>';
321
404
  }
322
405
 
323
- getTraces() {
324
- return this.traces;
406
+ html += '</div>';
407
+
408
+ // Orphans section
409
+ if (r.orphans && r.orphans.length > 0) {
410
+ html += '<div class="process-health-card">';
411
+ html += '<h4>Orphan Processes (' + r.orphans.length + ')</h4>';
412
+ html += '<table class="orphan-table"><thead><tr>';
413
+ html += '<th>PID</th><th>CPU%</th><th>MEM%</th><th>Uptime</th><th>Command</th>';
414
+ html += '</tr></thead><tbody>';
415
+ for (const o of r.orphans) {
416
+ html += '<tr>';
417
+ html += '<td>' + o.pid + '</td>';
418
+ html += '<td>' + escapeHtml(o.cpu) + '</td>';
419
+ html += '<td>' + escapeHtml(o.mem) + '</td>';
420
+ html += '<td>' + escapeHtml(o.elapsed) + '</td>';
421
+ html += '<td title="' + escapeHtml(o.cmdline || o.command) + '">' + escapeHtml(o.command) + '</td>';
422
+ html += '</tr>';
423
+ }
424
+ html += '</tbody></table></div>';
325
425
  }
326
426
 
327
- reconnect() {
328
- if (this.ws) {
329
- this.ws.close();
330
- }
331
- this.reconnectAttempts = 0;
332
- this.connectWebSocket();
427
+ container.innerHTML = html;
428
+ }
429
+
430
+ // Public methods for debugging
431
+ getStats() {
432
+ return this.stats;
433
+ }
434
+
435
+ getTraces() {
436
+ return this.traces;
437
+ }
438
+
439
+ reconnect() {
440
+ if (this.ws) {
441
+ this.ws.close();
333
442
  }
443
+ this.reconnectAttempts = 0;
444
+ this.connectWebSocket();
445
+ }
334
446
  }
335
447
 
336
448
  // Initialize dashboard when page loads
337
449
  document.addEventListener('DOMContentLoaded', () => {
338
- window.dashboard = new AgentFlowDashboard();
450
+ window.dashboard = new AgentFlowDashboard();
339
451
  });
340
452
 
341
453
  // Expose dashboard for debugging
342
- window.AgentFlowDashboard = AgentFlowDashboard;
454
+ window.AgentFlowDashboard = AgentFlowDashboard;