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.
- package/bin/dashboard.js +7 -5
- package/dist/chunk-L24LYP6L.js +515 -0
- package/dist/cli.cjs +665 -0
- package/dist/cli.js +120 -0
- package/dist/index.cjs +191 -236
- package/dist/index.js +5 -557
- package/dist/public/dashboard.js +365 -253
- package/dist/public/index.html +115 -0
- package/package.json +5 -4
- package/public/dashboard.js +365 -253
- package/public/index.html +115 -0
package/public/dashboard.js
CHANGED
|
@@ -1,166 +1,177 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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, '<')
|
|
6
|
+
.replace(/>/g, '>')
|
|
7
|
+
.replace(/"/g, '"')
|
|
8
|
+
.replace(/'/g, ''');
|
|
9
|
+
}
|
|
20
10
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
70
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
110
|
-
|
|
111
|
-
|
|
120
|
+
if (tracesRes.ok) {
|
|
121
|
+
this.traces = await tracesRes.json();
|
|
122
|
+
}
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
124
|
+
if (statsRes.ok) {
|
|
125
|
+
this.stats = await statsRes.json();
|
|
126
|
+
}
|
|
116
127
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
128
|
+
this.updateUI();
|
|
129
|
+
} catch (error) {
|
|
130
|
+
console.error('Error loading initial data:', error);
|
|
121
131
|
}
|
|
132
|
+
}
|
|
122
133
|
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
141
|
+
}
|
|
142
|
+
} catch (error) {
|
|
143
|
+
console.error('Error refreshing stats:', error);
|
|
140
144
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
154
|
-
|
|
164
|
+
updateStats() {
|
|
165
|
+
const statsGrid = document.getElementById('statsGrid');
|
|
155
166
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
167
|
+
if (!this.stats) {
|
|
168
|
+
statsGrid.innerHTML = '<div class="loading">Loading statistics...</div>';
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
160
171
|
|
|
161
|
-
|
|
172
|
+
const successRate = Math.round(this.stats.globalSuccessRate * 10) / 10;
|
|
162
173
|
|
|
163
|
-
|
|
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
|
-
|
|
184
|
-
|
|
194
|
+
updateAgents() {
|
|
195
|
+
const agentList = document.getElementById('agentList');
|
|
185
196
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
197
|
+
if (!this.stats || !this.stats.topAgents) {
|
|
198
|
+
agentList.innerHTML = '<div class="loading">Loading agents...</div>';
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
190
201
|
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
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
|
-
|
|
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
|
-
|
|
218
|
+
})
|
|
219
|
+
.join('');
|
|
207
220
|
|
|
208
|
-
|
|
209
|
-
|
|
221
|
+
agentList.innerHTML = agentItems;
|
|
222
|
+
}
|
|
210
223
|
|
|
211
|
-
|
|
212
|
-
|
|
224
|
+
updateTraces() {
|
|
225
|
+
const tracesList = document.getElementById('tracesList');
|
|
213
226
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
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
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
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
|
-
|
|
244
|
-
|
|
245
|
-
|
|
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
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
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
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
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
|
-
//
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
324
|
-
|
|
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
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
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
|
-
|
|
450
|
+
window.dashboard = new AgentFlowDashboard();
|
|
339
451
|
});
|
|
340
452
|
|
|
341
453
|
// Expose dashboard for debugging
|
|
342
|
-
window.AgentFlowDashboard = AgentFlowDashboard;
|
|
454
|
+
window.AgentFlowDashboard = AgentFlowDashboard;
|