EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
dashboardWS.js
Go to the documentation of this file.
1/**
2 * WebSocket Client for Dashboard
3 *
4 * Manages real-time communication with backend:
5 * - Subscribes to agent data streams
6 * - Receives metrics, process, service updates
7 * - Handles script execution output
8 * - Live logs and events
9 */
10
11class DashboardWebSocket {
12 constructor() {
13 this.ws = null;
14 this.connectionId = null;
15 this.connected = false;
16 this.authenticated = false;
17 this.subscriptions = new Set();
18 this.listeners = new Map();
19 this.reconnectAttempts = 0;
20 this.maxReconnectAttempts = 999;
21 this.baseDelay = 1000;
22 this.maxDelay = 30000;
23 this.reconnectTimer = null;
24 this.pendingRequests = new Map();
25 }
26
27 /**
28 * Connect to backend WebSocket
29 */
30 connect() {
31 // Use environment variable for WebSocket URL, or construct from API URL
32 // For production, this should point to the backend server (rmm-psa-backend-t9f7k.ondigitalocean.app)
33 let wsUrl;
34
35 if (import.meta.env.VITE_WS_URL) {
36 wsUrl = import.meta.env.VITE_WS_URL;
37 } else if (import.meta.env.VITE_API_URL) {
38 // Convert API URL to WebSocket URL
39 const apiUrl = import.meta.env.VITE_API_URL.replace('/api', '');
40 wsUrl = apiUrl.replace(/^http/, 'ws') + '/dashboard-ws';
41 } else {
42 // Fallback to same host (for local development)
43 const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
44 const host = window.location.hostname;
45 const port = window.location.port;
46 wsUrl = port ? `${protocol}//${host}:${port}/dashboard-ws` : `${protocol}//${host}/dashboard-ws`;
47 }
48
49 console.log('[DashboardWS] Connecting to:', wsUrl);
50
51 this.ws = new WebSocket(wsUrl);
52
53 this.ws.onopen = () => {
54 console.log('[DashboardWS] ✅ Connected');
55 this.connected = true;
56 this.reconnectAttempts = 0;
57
58 // Emit connected event
59 this.emit('connected');
60
61 // Authenticate with JWT token
62 const token = localStorage.getItem('token');
63 if (token) {
64 this.send({
65 type: 'authenticate',
66 token: token
67 });
68 } else {
69 console.warn('[DashboardWS] No authentication token found');
70 }
71 };
72
73 this.ws.onclose = (event) => {
74 console.log('[DashboardWS] Connection closed:', event.code, event.reason);
75 this.connected = false;
76 this.authenticated = false;
77 this.connectionId = null;
78 this.emit('disconnected');
79 this.scheduleReconnect();
80 };
81
82 this.ws.onerror = (error) => {
83 console.error('[DashboardWS] Error:', error);
84 this.emit('error', error);
85 };
86
87 this.ws.onmessage = (event) => {
88 try {
89 const msg = JSON.parse(event.data);
90 this.handleMessage(msg);
91 } catch (err) {
92 console.error('[DashboardWS] Failed to parse message:', err);
93 }
94 };
95 }
96
97 /**
98 * Schedule reconnection with exponential backoff
99 */
100 scheduleReconnect() {
101 if (this.reconnectTimer) {
102 clearTimeout(this.reconnectTimer);
103 }
104
105 this.reconnectAttempts++;
106 const delay = Math.min(
107 this.baseDelay * Math.pow(2, this.reconnectAttempts - 1),
108 this.maxDelay
109 );
110
111 console.log(`[DashboardWS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
112
113 this.reconnectTimer = setTimeout(() => {
114 this.connect();
115 }, delay);
116 }
117
118 /**
119 * Handle incoming messages
120 */
121 handleMessage(msg) {
122 console.log('[DashboardWS] Received:', msg.type);
123
124 switch (msg.type) {
125 case 'welcome':
126 this.connectionId = msg.connection_id;
127 console.log('[DashboardWS] Connection ID:', this.connectionId);
128 break;
129
130 case 'authenticated':
131 console.log('[DashboardWS] Authenticated as user:', msg.user_id);
132 this.authenticated = true;
133 this.emit('authenticated', msg);
134
135 // Now resubscribe to agents after authentication
136 if (this.subscriptions.size > 0) {
137 this.subscribe(Array.from(this.subscriptions));
138 }
139 break;
140
141 case 'auth_error':
142 console.error('[DashboardWS] Authentication failed:', msg.error);
143 this.authenticated = false;
144 this.emit('auth_error', msg.error);
145 break;
146
147 case 'subscribed':
148 console.log('[DashboardWS] Subscribed to:', msg.agent_uuids);
149 break;
150
151 case 'metrics_update':
152 this.emit('metrics', msg.agent_uuid, msg.data);
153 break;
154
155 case 'processes_snapshot':
156 this.emit('processes', msg.agent_uuid, msg.processes);
157 break;
158
159 case 'services_snapshot':
160 this.emit('services', msg.agent_uuid, msg.services);
161 break;
162
163 case 'script_output':
164 this.emit('script_output', msg.execution_id, msg.line);
165 break;
166
167 case 'script_complete':
168 this.emit('script_complete', msg.execution_id, msg.exit_code);
169 break;
170
171 case 'script_error':
172 this.emit('script_error', msg.execution_id, msg.error);
173 break;
174
175 case 'agent_log':
176 this.emit('agent_log', msg.agent_uuid, msg.level, msg.message);
177 break;
178
179 case 'event_log':
180 this.emit('event_log', msg.agent_uuid, msg.event);
181 break;
182
183 case 'agent_disconnected':
184 this.emit('agent_disconnected', msg.agent_uuid);
185 break;
186
187 case 'response':
188 // Handle response to request
189 if (msg.request_id) {
190 const pending = this.pendingRequests.get(msg.request_id);
191 if (pending) {
192 clearTimeout(pending.timeout);
193 pending.resolve(msg.data);
194 this.pendingRequests.delete(msg.request_id);
195 }
196 }
197 break;
198
199 default:
200 console.log('[DashboardWS] Unknown message type:', msg.type);
201 }
202 }
203
204 /**
205 * Subscribe to agent updates
206 */
207 subscribe(agent_uuids) {
208 if (!Array.isArray(agent_uuids)) {
209 agent_uuids = [agent_uuids];
210 }
211
212 // Add to subscriptions set
213 agent_uuids.forEach(uuid => this.subscriptions.add(uuid));
214
215 if (!this.connected || !this.ws) {
216 console.log('[DashboardWS] Not connected, will subscribe on connect');
217 return;
218 }
219
220 this.send({
221 type: 'subscribe',
222 agent_uuids
223 });
224 }
225
226 /**
227 * Unsubscribe from agent updates
228 */
229 unsubscribe(agent_uuids) {
230 if (!Array.isArray(agent_uuids)) {
231 agent_uuids = [agent_uuids];
232 }
233
234 // Remove from subscriptions set
235 agent_uuids.forEach(uuid => this.subscriptions.delete(uuid));
236
237 if (!this.connected || !this.ws) {
238 return;
239 }
240
241 this.send({
242 type: 'unsubscribe',
243 agent_uuids
244 });
245 }
246
247 /**
248 * Request processes snapshot
249 */
250 requestProcesses(agent_uuid) {
251 return this.sendRequest({
252 type: 'request_processes',
253 agent_uuid
254 });
255 }
256
257 /**
258 * Request services snapshot
259 */
260 requestServices(agent_uuid) {
261 return this.sendRequest({
262 type: 'request_services',
263 agent_uuid
264 });
265 }
266
267 /**
268 * Execute script
269 */
270 executeScript(agent_uuid, script_id, code) {
271 this.send({
272 type: 'execute_script',
273 agent_uuid,
274 script_id,
275 code
276 });
277 }
278
279 /**
280 * Kill process
281 */
282 killProcess(agent_uuid, pid) {
283 return this.sendRequest({
284 type: 'kill_process',
285 agent_uuid,
286 pid
287 });
288 }
289
290 /**
291 * Control service (start/stop/restart)
292 */
293 controlService(agent_uuid, service_name, action) {
294 return this.sendRequest({
295 type: 'control_service',
296 agent_uuid,
297 service_name,
298 action
299 });
300 }
301
302 /**
303 * Send message to backend
304 */
305 send(message) {
306 if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
307 console.warn('[DashboardWS] Cannot send, not connected');
308 return false;
309 }
310
311 try {
312 this.ws.send(JSON.stringify(message));
313 return true;
314 } catch (err) {
315 console.error('[DashboardWS] Failed to send:', err);
316 return false;
317 }
318 }
319
320 /**
321 * Send request and await response
322 */
323 sendRequest(message, timeout = 10000) {
324 return new Promise((resolve, reject) => {
325 const request_id = `req-${Date.now()}-${Math.random().toString(36).slice(2)}`;
326
327 const timer = setTimeout(() => {
328 this.pendingRequests.delete(request_id);
329 reject(new Error('Request timeout'));
330 }, timeout);
331
332 this.pendingRequests.set(request_id, { resolve, reject, timeout: timer });
333
334 const success = this.send({
335 ...message,
336 request_id
337 });
338
339 if (!success) {
340 clearTimeout(timer);
341 this.pendingRequests.delete(request_id);
342 reject(new Error('Not connected'));
343 }
344 });
345 }
346
347 /**
348 * Add event listener
349 */
350 on(event, callback) {
351 if (!this.listeners.has(event)) {
352 this.listeners.set(event, []);
353 }
354 this.listeners.get(event).push(callback);
355 }
356
357 /**
358 * Remove event listener
359 */
360 off(event, callback) {
361 if (!this.listeners.has(event)) return;
362 const callbacks = this.listeners.get(event);
363 const index = callbacks.indexOf(callback);
364 if (index > -1) {
365 callbacks.splice(index, 1);
366 }
367 }
368
369 /**
370 * Emit event
371 */
372 emit(event, ...args) {
373 if (!this.listeners.has(event)) return;
374 const callbacks = this.listeners.get(event);
375 callbacks.forEach(callback => {
376 try {
377 callback(...args);
378 } catch (err) {
379 console.error(`[DashboardWS] Error in ${event} callback:`, err);
380 }
381 });
382 }
383
384 /**
385 * Disconnect
386 */
387 disconnect() {
388 if (this.reconnectTimer) {
389 clearTimeout(this.reconnectTimer);
390 this.reconnectTimer = null;
391 }
392
393 if (this.ws) {
394 this.ws.close();
395 this.ws = null;
396 }
397
398 this.connected = false;
399 this.connectionId = null;
400 this.subscriptions.clear();
401 this.listeners.clear();
402 }
403}
404
405// Create singleton instance
406const dashboardWS = new DashboardWebSocket();
407
408// Auto-connect on load
409if (typeof window !== 'undefined') {
410 window.addEventListener('load', () => {
411 dashboardWS.connect();
412 });
413}
414
415export default dashboardWS;