2 * WebSocket Client for Dashboard
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
11class DashboardWebSocket {
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();
28 * Connect to backend WebSocket
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)
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';
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`;
49 console.log('[DashboardWS] Connecting to:', wsUrl);
51 this.ws = new WebSocket(wsUrl);
53 this.ws.onopen = () => {
54 console.log('[DashboardWS] ✅ Connected');
55 this.connected = true;
56 this.reconnectAttempts = 0;
58 // Emit connected event
59 this.emit('connected');
61 // Authenticate with JWT token
62 const token = localStorage.getItem('token');
69 console.warn('[DashboardWS] No authentication token found');
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();
82 this.ws.onerror = (error) => {
83 console.error('[DashboardWS] Error:', error);
84 this.emit('error', error);
87 this.ws.onmessage = (event) => {
89 const msg = JSON.parse(event.data);
90 this.handleMessage(msg);
92 console.error('[DashboardWS] Failed to parse message:', err);
98 * Schedule reconnection with exponential backoff
100 scheduleReconnect() {
101 if (this.reconnectTimer) {
102 clearTimeout(this.reconnectTimer);
105 this.reconnectAttempts++;
106 const delay = Math.min(
107 this.baseDelay * Math.pow(2, this.reconnectAttempts - 1),
111 console.log(`[DashboardWS] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
113 this.reconnectTimer = setTimeout(() => {
119 * Handle incoming messages
122 console.log('[DashboardWS] Received:', msg.type);
126 this.connectionId = msg.connection_id;
127 console.log('[DashboardWS] Connection ID:', this.connectionId);
130 case 'authenticated':
131 console.log('[DashboardWS] Authenticated as user:', msg.user_id);
132 this.authenticated = true;
133 this.emit('authenticated', msg);
135 // Now resubscribe to agents after authentication
136 if (this.subscriptions.size > 0) {
137 this.subscribe(Array.from(this.subscriptions));
142 console.error('[DashboardWS] Authentication failed:', msg.error);
143 this.authenticated = false;
144 this.emit('auth_error', msg.error);
148 console.log('[DashboardWS] Subscribed to:', msg.agent_uuids);
151 case 'metrics_update':
152 this.emit('metrics', msg.agent_uuid, msg.data);
155 case 'processes_snapshot':
156 this.emit('processes', msg.agent_uuid, msg.processes);
159 case 'services_snapshot':
160 this.emit('services', msg.agent_uuid, msg.services);
163 case 'script_output':
164 this.emit('script_output', msg.execution_id, msg.line);
167 case 'script_complete':
168 this.emit('script_complete', msg.execution_id, msg.exit_code);
172 this.emit('script_error', msg.execution_id, msg.error);
176 this.emit('agent_log', msg.agent_uuid, msg.level, msg.message);
180 this.emit('event_log', msg.agent_uuid, msg.event);
183 case 'agent_disconnected':
184 this.emit('agent_disconnected', msg.agent_uuid);
188 // Handle response to request
189 if (msg.request_id) {
190 const pending = this.pendingRequests.get(msg.request_id);
192 clearTimeout(pending.timeout);
193 pending.resolve(msg.data);
194 this.pendingRequests.delete(msg.request_id);
200 console.log('[DashboardWS] Unknown message type:', msg.type);
205 * Subscribe to agent updates
207 subscribe(agent_uuids) {
208 if (!Array.isArray(agent_uuids)) {
209 agent_uuids = [agent_uuids];
212 // Add to subscriptions set
213 agent_uuids.forEach(uuid => this.subscriptions.add(uuid));
215 if (!this.connected || !this.ws) {
216 console.log('[DashboardWS] Not connected, will subscribe on connect');
227 * Unsubscribe from agent updates
229 unsubscribe(agent_uuids) {
230 if (!Array.isArray(agent_uuids)) {
231 agent_uuids = [agent_uuids];
234 // Remove from subscriptions set
235 agent_uuids.forEach(uuid => this.subscriptions.delete(uuid));
237 if (!this.connected || !this.ws) {
248 * Request processes snapshot
250 requestProcesses(agent_uuid) {
251 return this.sendRequest({
252 type: 'request_processes',
258 * Request services snapshot
260 requestServices(agent_uuid) {
261 return this.sendRequest({
262 type: 'request_services',
270 executeScript(agent_uuid, script_id, code) {
272 type: 'execute_script',
282 killProcess(agent_uuid, pid) {
283 return this.sendRequest({
284 type: 'kill_process',
291 * Control service (start/stop/restart)
293 controlService(agent_uuid, service_name, action) {
294 return this.sendRequest({
295 type: 'control_service',
303 * Send message to backend
306 if (!this.connected || !this.ws || this.ws.readyState !== WebSocket.OPEN) {
307 console.warn('[DashboardWS] Cannot send, not connected');
312 this.ws.send(JSON.stringify(message));
315 console.error('[DashboardWS] Failed to send:', err);
321 * Send request and await response
323 sendRequest(message, timeout = 10000) {
324 return new Promise((resolve, reject) => {
325 const request_id = `req-${Date.now()}-${Math.random().toString(36).slice(2)}`;
327 const timer = setTimeout(() => {
328 this.pendingRequests.delete(request_id);
329 reject(new Error('Request timeout'));
332 this.pendingRequests.set(request_id, { resolve, reject, timeout: timer });
334 const success = this.send({
341 this.pendingRequests.delete(request_id);
342 reject(new Error('Not connected'));
350 on(event, callback) {
351 if (!this.listeners.has(event)) {
352 this.listeners.set(event, []);
354 this.listeners.get(event).push(callback);
358 * Remove event listener
360 off(event, callback) {
361 if (!this.listeners.has(event)) return;
362 const callbacks = this.listeners.get(event);
363 const index = callbacks.indexOf(callback);
365 callbacks.splice(index, 1);
372 emit(event, ...args) {
373 if (!this.listeners.has(event)) return;
374 const callbacks = this.listeners.get(event);
375 callbacks.forEach(callback => {
379 console.error(`[DashboardWS] Error in ${event} callback:`, err);
388 if (this.reconnectTimer) {
389 clearTimeout(this.reconnectTimer);
390 this.reconnectTimer = null;
398 this.connected = false;
399 this.connectionId = null;
400 this.subscriptions.clear();
401 this.listeners.clear();
405// Create singleton instance
406const dashboardWS = new DashboardWebSocket();
408// Auto-connect on load
409if (typeof window !== 'undefined') {
410 window.addEventListener('load', () => {
411 dashboardWS.connect();
415export default dashboardWS;