shell-mirror 1.5.55 → 1.5.57

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.55",
3
+ "version": "1.5.57",
4
4
  "description": "Access your Mac shell from any device securely. Perfect for mobile coding with Claude Code CLI, Gemini CLI, and any shell tool.",
5
5
  "main": "server.js",
6
6
  "bin": {
@@ -42,12 +42,28 @@ body {
42
42
  opacity: 0.8;
43
43
  }
44
44
 
45
+ .header-right {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 20px;
49
+ }
50
+
45
51
  .user-section {
46
52
  display: flex;
47
53
  align-items: center;
48
54
  gap: 15px;
49
55
  }
50
56
 
57
+ .dashboard-controls {
58
+ display: flex;
59
+ align-items: center;
60
+ gap: 12px;
61
+ padding: 8px 16px;
62
+ background: rgba(255, 255, 255, 0.15);
63
+ border-radius: 8px;
64
+ backdrop-filter: blur(10px);
65
+ }
66
+
51
67
  .user-info {
52
68
  display: flex;
53
69
  align-items: center;
@@ -106,6 +122,64 @@ body {
106
122
  display: block;
107
123
  }
108
124
 
125
+ /* Dashboard Controls */
126
+ .refresh-btn {
127
+ background: rgba(255, 255, 255, 0.2);
128
+ border: 1px solid rgba(255, 255, 255, 0.3);
129
+ border-radius: 50%;
130
+ color: white;
131
+ width: 36px;
132
+ height: 36px;
133
+ cursor: pointer;
134
+ transition: all 0.2s ease;
135
+ display: flex;
136
+ align-items: center;
137
+ justify-content: center;
138
+ font-size: 1rem;
139
+ }
140
+
141
+ .refresh-btn:hover:not(:disabled) {
142
+ background: rgba(255, 255, 255, 0.3);
143
+ transform: scale(1.05);
144
+ }
145
+
146
+ .refresh-btn.loading {
147
+ opacity: 0.7;
148
+ cursor: not-allowed;
149
+ }
150
+
151
+ .refresh-btn.loading .refresh-icon {
152
+ animation: spin 1s linear infinite;
153
+ }
154
+
155
+ .connection-status {
156
+ font-size: 0.8rem;
157
+ font-weight: 500;
158
+ white-space: nowrap;
159
+ padding: 4px 8px;
160
+ border-radius: 12px;
161
+ background: rgba(255, 255, 255, 0.1);
162
+ }
163
+
164
+ .connection-status.connected {
165
+ color: #4caf50;
166
+ }
167
+
168
+ .connection-status.disconnected {
169
+ color: #ff9800;
170
+ }
171
+
172
+ .connection-status.error,
173
+ .connection-status.failed {
174
+ color: #f44336;
175
+ }
176
+
177
+ .refresh-status {
178
+ font-size: 0.75rem;
179
+ opacity: 0.8;
180
+ white-space: nowrap;
181
+ }
182
+
109
183
  /* Buttons */
110
184
  .btn-primary {
111
185
  background: #4285F4;
@@ -524,6 +598,28 @@ body {
524
598
  text-align: center;
525
599
  }
526
600
 
601
+ .header-right {
602
+ flex-direction: column;
603
+ gap: 10px;
604
+ width: 100%;
605
+ }
606
+
607
+ .dashboard-controls {
608
+ justify-content: center;
609
+ flex-wrap: wrap;
610
+ }
611
+
612
+ .refresh-status,
613
+ .connection-status {
614
+ font-size: 0.7rem;
615
+ }
616
+
617
+ .agent-notification {
618
+ right: 10px;
619
+ left: 10px;
620
+ text-align: center;
621
+ }
622
+
527
623
  .dashboard-main {
528
624
  padding: 20px 15px;
529
625
  }
@@ -694,4 +790,66 @@ body {
694
790
  border-top: 1px solid #eee;
695
791
  padding-top: 16px;
696
792
  text-align: center;
793
+ }
794
+
795
+ /* Agent Notifications */
796
+ .agent-notification {
797
+ position: fixed;
798
+ top: 100px;
799
+ right: 20px;
800
+ background: white;
801
+ color: #333;
802
+ padding: 12px 20px;
803
+ border-radius: 8px;
804
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
805
+ font-size: 0.9rem;
806
+ font-weight: 500;
807
+ z-index: 3000;
808
+ animation: slideInRight 0.3s ease-out, fadeOut 0.3s ease-in 4.7s forwards;
809
+ border-left: 4px solid;
810
+ }
811
+
812
+ .agent-notification.connected {
813
+ border-left-color: #4caf50;
814
+ }
815
+
816
+ .agent-notification.disconnected {
817
+ border-left-color: #f44336;
818
+ }
819
+
820
+ @keyframes slideInRight {
821
+ from {
822
+ transform: translateX(100%);
823
+ opacity: 0;
824
+ }
825
+ to {
826
+ transform: translateX(0);
827
+ opacity: 1;
828
+ }
829
+ }
830
+
831
+ @keyframes fadeOut {
832
+ to {
833
+ opacity: 0;
834
+ transform: translateX(100%);
835
+ }
836
+ }
837
+
838
+ /* API Error Display */
839
+ .api-error {
840
+ text-align: center;
841
+ padding: 20px;
842
+ background: #fff5f5;
843
+ border: 1px solid #fed7d7;
844
+ border-radius: 8px;
845
+ color: #c53030;
846
+ }
847
+
848
+ .api-error p {
849
+ margin-bottom: 15px;
850
+ font-weight: 500;
851
+ }
852
+
853
+ .api-error button {
854
+ margin-top: 10px;
697
855
  }
@@ -61,8 +61,10 @@
61
61
  <h1>Shell Mirror</h1>
62
62
  <span class="subtitle">Dashboard</span>
63
63
  </div>
64
- <div class="user-section" id="user-section">
65
- <!-- Dynamic content based on auth status -->
64
+ <div class="header-right">
65
+ <div class="user-section" id="user-section">
66
+ <!-- Dynamic content based on auth status -->
67
+ </div>
66
68
  </div>
67
69
  </div>
68
70
  </header>
@@ -6,6 +6,12 @@ class ShellMirrorDashboard {
6
6
  this.agents = [];
7
7
  this.sessions = [];
8
8
  this.agentSessions = {}; // Maps agentId to sessions array
9
+ this.websocket = null;
10
+ this.reconnectAttempts = 0;
11
+ this.maxReconnectAttempts = 10;
12
+ this.refreshInterval = null;
13
+ this.lastRefresh = null;
14
+ this.isRefreshing = false;
9
15
  this.init();
10
16
  }
11
17
 
@@ -38,6 +44,7 @@ class ShellMirrorDashboard {
38
44
  this.user = authStatus.user;
39
45
  await this.loadDashboardData();
40
46
  this.renderAuthenticatedDashboard();
47
+ this.setupWebSocket(); // Setup real-time connection
41
48
  this.startAutoRefresh(); // Start auto-refresh for authenticated users
42
49
  } else {
43
50
  this.renderUnauthenticatedDashboard();
@@ -51,14 +58,285 @@ class ShellMirrorDashboard {
51
58
  }
52
59
 
53
60
  startAutoRefresh() {
54
- // Refresh agent data every 30 seconds to detect disconnected agents
61
+ // Clear any existing interval
62
+ if (this.refreshInterval) {
63
+ clearInterval(this.refreshInterval);
64
+ }
65
+
66
+ // Refresh agent data every 10 seconds (reduced from 30s)
55
67
  this.refreshInterval = setInterval(async () => {
56
- if (this.isAuthenticated) {
57
- await this.loadDashboardData();
58
- // Only re-render the agents section to avoid full page flash
59
- this.updateAgentsDisplay();
68
+ if (this.isAuthenticated && !this.isRefreshing) {
69
+ await this.refreshDashboardData();
60
70
  }
61
- }, 30000);
71
+ }, 10000);
72
+ }
73
+
74
+ async refreshDashboardData() {
75
+ this.isRefreshing = true;
76
+ try {
77
+ await this.loadDashboardData();
78
+ this.updateAgentsDisplay();
79
+ this.updateLastRefreshTime();
80
+ } catch (error) {
81
+ console.error('Auto-refresh failed:', error);
82
+ // Implement exponential backoff on failure
83
+ clearInterval(this.refreshInterval);
84
+ const backoffDelay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
85
+ setTimeout(() => {
86
+ if (this.isAuthenticated) {
87
+ this.startAutoRefresh();
88
+ this.reconnectAttempts++;
89
+ }
90
+ }, backoffDelay);
91
+ } finally {
92
+ this.isRefreshing = false;
93
+ }
94
+ }
95
+
96
+ updateLastRefreshTime() {
97
+ this.lastRefresh = Date.now();
98
+ const refreshStatus = document.getElementById('refresh-status');
99
+ if (refreshStatus) {
100
+ refreshStatus.textContent = `Last updated: ${new Date(this.lastRefresh).toLocaleTimeString()}`;
101
+ }
102
+ }
103
+
104
+ setupWebSocket() {
105
+ // Detect production environment
106
+ const isProduction = window.location.hostname === 'shellmirror.app' ||
107
+ window.location.hostname === 'www.shellmirror.app' ||
108
+ window.location.hostname === 'www.igori.eu' ||
109
+ window.location.hostname === 'igori.eu';
110
+
111
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
112
+ let wsUrl;
113
+
114
+ if (isProduction) {
115
+ // For production, try the Heroku WebSocket app URL
116
+ // This may need to be adjusted based on actual deployment architecture
117
+ wsUrl = `wss://shell-mirror-30aa5479ceaf.herokuapp.com/?role=dashboard`;
118
+ console.log('[DASHBOARD] 🌐 Production environment detected, using Heroku WebSocket URL');
119
+ } else {
120
+ // For development, use same host
121
+ wsUrl = `${protocol}//${window.location.host}/?role=dashboard`;
122
+ }
123
+
124
+ console.log('[DASHBOARD] 🔌 Connecting to WebSocket:', wsUrl);
125
+
126
+ try {
127
+ this.websocket = new WebSocket(wsUrl);
128
+
129
+ this.websocket.onopen = () => {
130
+ console.log('[DASHBOARD] ✅ WebSocket connected to:', wsUrl);
131
+ this.reconnectAttempts = 0;
132
+ this.updateConnectionStatus('connected');
133
+
134
+ // Send authentication info if available
135
+ const user = this.user;
136
+ if (user) {
137
+ console.log('[DASHBOARD] 🔐 Sending authentication to WebSocket');
138
+ this.websocket.send(JSON.stringify({
139
+ type: 'authenticate',
140
+ userId: user.id || user.email,
141
+ email: user.email
142
+ }));
143
+ }
144
+ };
145
+
146
+ this.websocket.onmessage = (event) => {
147
+ this.handleWebSocketMessage(event.data);
148
+ };
149
+
150
+ this.websocket.onclose = (event) => {
151
+ const closeReasons = {
152
+ 1000: 'Normal Closure',
153
+ 1001: 'Going Away',
154
+ 1002: 'Protocol Error',
155
+ 1003: 'Unsupported Data',
156
+ 1004: 'Reserved',
157
+ 1005: 'No Status Rcvd',
158
+ 1006: 'Abnormal Closure',
159
+ 1007: 'Invalid frame payload data',
160
+ 1008: 'Policy Violation',
161
+ 1009: 'Message too big',
162
+ 1010: 'Mandatory Extension',
163
+ 1011: 'Internal Server Error',
164
+ 1015: 'TLS Handshake'
165
+ };
166
+
167
+ const reason = closeReasons[event.code] || 'Unknown';
168
+ console.log(`[DASHBOARD] 🔌 WebSocket closed: ${event.code} (${reason})`, event.reason);
169
+
170
+ if (event.code === 1008) {
171
+ console.error('[DASHBOARD] ❌ Authentication required - WebSocket rejected connection');
172
+ } else if (event.code === 1006) {
173
+ console.error('[DASHBOARD] ❌ Abnormal closure - WebSocket endpoint may not exist');
174
+ }
175
+
176
+ this.updateConnectionStatus('disconnected');
177
+ this.attemptReconnect();
178
+ };
179
+
180
+ this.websocket.onerror = (error) => {
181
+ console.error('[DASHBOARD] ❌ WebSocket error:', error);
182
+ console.log('[DASHBOARD] 🔍 WebSocket URL attempted:', wsUrl);
183
+ this.updateConnectionStatus('error');
184
+ };
185
+
186
+ // Send periodic ping to keep connection alive
187
+ setInterval(() => {
188
+ if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
189
+ this.websocket.send(JSON.stringify({ type: 'ping' }));
190
+ }
191
+ }, 30000);
192
+
193
+ } catch (error) {
194
+ console.error('[DASHBOARD] ❌ Failed to setup WebSocket:', error);
195
+ this.updateConnectionStatus('error');
196
+ }
197
+ }
198
+
199
+ handleWebSocketMessage(data) {
200
+ try {
201
+ const message = JSON.parse(data);
202
+ console.log('[DASHBOARD] 📨 WebSocket message:', message);
203
+
204
+ switch (message.type) {
205
+ case 'agent-list':
206
+ // Initial agent list or refresh
207
+ this.handleAgentListUpdate(message.agents);
208
+ break;
209
+ case 'agent-connected':
210
+ this.handleAgentConnected(message.agentId);
211
+ break;
212
+ case 'agent-disconnected':
213
+ this.handleAgentDisconnected(message.agentId);
214
+ break;
215
+ case 'pong':
216
+ console.log('[DASHBOARD] 🏓 Received pong');
217
+ break;
218
+ default:
219
+ console.log('[DASHBOARD] ❓ Unknown message type:', message.type);
220
+ }
221
+ } catch (error) {
222
+ console.error('[DASHBOARD] ❌ Error handling WebSocket message:', error);
223
+ }
224
+ }
225
+
226
+ handleAgentListUpdate(agentIds) {
227
+ console.log('[DASHBOARD] 📋 Agent list update:', agentIds);
228
+ // This is just the initial list of agent IDs, we still need to load full data
229
+ this.refreshDashboardData();
230
+ }
231
+
232
+ handleAgentConnected(agentId) {
233
+ console.log('[DASHBOARD] ✅ Agent connected:', agentId);
234
+ this.showAgentNotification(agentId, 'connected');
235
+ // Refresh data to get the new agent's details
236
+ this.refreshDashboardData();
237
+ }
238
+
239
+ handleAgentDisconnected(agentId) {
240
+ console.log('[DASHBOARD] ❌ Agent disconnected:', agentId);
241
+ this.showAgentNotification(agentId, 'disconnected');
242
+ // Refresh data to update agent status
243
+ this.refreshDashboardData();
244
+ }
245
+
246
+ showAgentNotification(agentId, status) {
247
+ const message = status === 'connected'
248
+ ? `🟢 Agent ${agentId} connected`
249
+ : `🔴 Agent ${agentId} disconnected`;
250
+
251
+ // Create notification element
252
+ const notification = document.createElement('div');
253
+ notification.className = `agent-notification ${status}`;
254
+ notification.textContent = message;
255
+
256
+ // Add to page
257
+ document.body.appendChild(notification);
258
+
259
+ // Auto-remove after 5 seconds
260
+ setTimeout(() => {
261
+ if (document.body.contains(notification)) {
262
+ document.body.removeChild(notification);
263
+ }
264
+ }, 5000);
265
+ }
266
+
267
+ attemptReconnect() {
268
+ if (this.reconnectAttempts < this.maxReconnectAttempts && this.isAuthenticated) {
269
+ const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
270
+ console.log(`[DASHBOARD] 🔄 Attempting reconnect in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
271
+
272
+ setTimeout(() => {
273
+ this.reconnectAttempts++;
274
+ this.setupWebSocket();
275
+ }, delay);
276
+ } else {
277
+ console.log('[DASHBOARD] ❌ Max reconnection attempts reached, switching to HTTP-only mode');
278
+ this.updateConnectionStatus('offline');
279
+ this.enableHttpOnlyMode();
280
+ }
281
+ }
282
+
283
+ enableHttpOnlyMode() {
284
+ console.log('[DASHBOARD] 📡 Enabling HTTP-only mode (no real-time updates)');
285
+ this.websocket = null;
286
+
287
+ // Update UI to show HTTP-only mode
288
+ const connectionStatus = document.getElementById('connection-status');
289
+ if (connectionStatus) {
290
+ connectionStatus.textContent = '📡 HTTP Only';
291
+ connectionStatus.className = 'connection-status offline';
292
+ connectionStatus.title = 'Real-time updates unavailable - using polling';
293
+ }
294
+
295
+ // Continue with HTTP polling only
296
+ console.log('[DASHBOARD] ✅ Dashboard running in HTTP-only mode');
297
+ }
298
+
299
+ updateConnectionStatus(status) {
300
+ const connectionStatus = document.getElementById('connection-status');
301
+ if (connectionStatus) {
302
+ connectionStatus.className = `connection-status ${status}`;
303
+ const statusText = {
304
+ connected: '🟢 Live',
305
+ disconnected: '🟡 Reconnecting...',
306
+ error: '🔴 Connection Error',
307
+ failed: '🔴 Offline'
308
+ };
309
+ connectionStatus.textContent = statusText[status] || '❓ Unknown';
310
+ }
311
+ }
312
+
313
+ async manualRefresh() {
314
+ if (this.isRefreshing) {
315
+ console.log('[DASHBOARD] ⚠️ Refresh already in progress, ignoring manual request');
316
+ return;
317
+ }
318
+
319
+ console.log('[DASHBOARD] 🔄 Manual refresh triggered');
320
+
321
+ // Show loading state on refresh button
322
+ const refreshBtn = document.getElementById('refresh-btn');
323
+ if (refreshBtn) {
324
+ refreshBtn.classList.add('loading');
325
+ refreshBtn.disabled = true;
326
+ }
327
+
328
+ try {
329
+ await this.refreshDashboardData();
330
+ console.log('[DASHBOARD] ✅ Manual refresh completed');
331
+ } catch (error) {
332
+ console.error('[DASHBOARD] ❌ Manual refresh failed:', error);
333
+ } finally {
334
+ // Remove loading state
335
+ if (refreshBtn) {
336
+ refreshBtn.classList.remove('loading');
337
+ refreshBtn.disabled = false;
338
+ }
339
+ }
62
340
  }
63
341
 
64
342
  updateAgentsDisplay() {
@@ -92,15 +370,27 @@ class ShellMirrorDashboard {
92
370
 
93
371
  async loadDashboardData() {
94
372
  try {
95
- // Load active agents
96
- const agentsResponse = await fetch('/php-backend/api/agents-list.php');
373
+ // Load active agents with detailed debugging
374
+ console.log('[DASHBOARD] 📡 Fetching agents from API...');
375
+ const agentsResponse = await fetch('/php-backend/api/agents-list.php', {
376
+ credentials: 'include' // Include authentication cookies
377
+ });
378
+
379
+ console.log('[DASHBOARD] 🔍 API Response Status:', agentsResponse.status);
380
+ console.log('[DASHBOARD] 🔍 API Response Headers:', Object.fromEntries(agentsResponse.headers.entries()));
381
+
97
382
  const agentsData = await agentsResponse.json();
383
+ console.log('[DASHBOARD] 🔍 API Response Data:', agentsData);
98
384
 
99
385
  if (agentsData.success && agentsData.data && agentsData.data.agents) {
100
386
  this.agents = agentsData.data.agents;
387
+ console.log('[DASHBOARD] ✅ Loaded agents:', this.agents.length);
101
388
 
102
389
  // Load session data from localStorage (persisted from terminal connections)
103
390
  this.loadSessionsFromStorage();
391
+ } else {
392
+ console.warn('[DASHBOARD] ⚠️ No agents found in API response:', agentsData);
393
+ this.agents = [];
104
394
  }
105
395
 
106
396
  // TODO: Load session history when API is available
@@ -122,7 +412,25 @@ class ShellMirrorDashboard {
122
412
  ];
123
413
 
124
414
  } catch (error) {
125
- console.error('Failed to load dashboard data:', error);
415
+ console.error('[DASHBOARD] ❌ Failed to load dashboard data:', error);
416
+ this.agents = [];
417
+
418
+ // Show error in UI
419
+ const agentsCard = document.querySelector('.dashboard-card');
420
+ if (agentsCard) {
421
+ agentsCard.innerHTML = `
422
+ <div class="card-header">
423
+ <h2>🖥️ Active Agents</h2>
424
+ <span class="agent-count">Error</span>
425
+ </div>
426
+ <div class="card-content">
427
+ <div class="api-error">
428
+ <p>⚠️ Failed to load agents: ${error.message}</p>
429
+ <button onclick="dashboard.manualRefresh()" class="btn-primary">Retry</button>
430
+ </div>
431
+ </div>
432
+ `;
433
+ }
126
434
  }
127
435
  }
128
436
 
@@ -157,8 +465,15 @@ class ShellMirrorDashboard {
157
465
  }
158
466
 
159
467
  renderAuthenticatedDashboard() {
160
- // Update user section
468
+ // Update user section with refresh button and status
161
469
  document.getElementById('user-section').innerHTML = `
470
+ <div class="dashboard-controls">
471
+ <span id="connection-status" class="connection-status">🟡 Connecting...</span>
472
+ <span id="refresh-status" class="refresh-status">Initializing...</span>
473
+ <button id="refresh-btn" class="refresh-btn" onclick="dashboard.manualRefresh()" title="Refresh agents">
474
+ <span class="refresh-icon">🔄</span>
475
+ </button>
476
+ </div>
162
477
  <div class="user-info">
163
478
  <span class="user-name">${this.user.name || this.user.email}</span>
164
479
  <div class="user-dropdown">
@@ -669,4 +984,14 @@ function handleLogin() {
669
984
  let dashboard;
670
985
  document.addEventListener('DOMContentLoaded', () => {
671
986
  dashboard = new ShellMirrorDashboard();
987
+ });
988
+
989
+ // Cleanup on page unload
990
+ window.addEventListener('beforeunload', () => {
991
+ if (dashboard && dashboard.websocket) {
992
+ dashboard.websocket.close();
993
+ }
994
+ if (dashboard && dashboard.refreshInterval) {
995
+ clearInterval(dashboard.refreshInterval);
996
+ }
672
997
  });
package/server.js CHANGED
@@ -44,6 +44,7 @@ app.use(passport.session());
44
44
  // --- In-memory data stores ---
45
45
  const agents = new Map(); // Stores connected agent sockets
46
46
  const clients = new Map(); // Stores connected client (browser) sockets
47
+ const dashboards = new Map(); // Stores connected dashboard sockets
47
48
  const sessions = new Map(); // Maps session IDs to agent/client pairs
48
49
 
49
50
  // --- Environment Detection ---
@@ -150,7 +151,53 @@ wss.on('connection', (ws, req) => {
150
151
  const role = params.get('role');
151
152
  const agentId = params.get('agentId');
152
153
 
153
- if (role === 'discovery') {
154
+ if (role === 'dashboard') {
155
+ // --- Dashboard Connection ---
156
+ sessionParser(req, {}, () => {
157
+ let userId;
158
+ let isAuthenticated = false;
159
+
160
+ if (req.session && req.session.passport && req.session.passport.user) {
161
+ userId = req.session.passport.user.id;
162
+ isAuthenticated = true;
163
+ logToFile(`✅ Authenticated dashboard connected: ${userId}`);
164
+ } else if (isLocalEnvironment && !isProduction) {
165
+ // LOCAL_TESTING_ONLY: Allow unauthenticated connections for local testing
166
+ userId = `local-test-dashboard-${uuidv4()}`;
167
+ logToFile(`🔧 LOCAL TESTING: Unauthenticated dashboard connected: ${userId}`);
168
+ } else {
169
+ // Production: Reject unauthenticated connections
170
+ logToFile('❌ Unauthenticated dashboard rejected in production environment');
171
+ ws.close(1008, 'Authentication required');
172
+ return;
173
+ }
174
+
175
+ ws.userId = userId;
176
+ dashboards.set(userId, ws);
177
+
178
+ // Send initial agent list
179
+ const agentsList = Array.from(agents.keys()).map(id => ({ id }));
180
+ ws.send(JSON.stringify({ type: 'agent-list', agents: agentsList }));
181
+ logToFile(`📊 Sent initial agent list to dashboard: ${agentsList.length} agents`);
182
+
183
+ ws.on('close', () => {
184
+ logToFile(`🔌 Dashboard disconnected: ${userId}`);
185
+ dashboards.delete(userId);
186
+ });
187
+
188
+ ws.on('message', (message) => {
189
+ try {
190
+ const data = JSON.parse(message);
191
+ if (data.type === 'ping') {
192
+ ws.send(JSON.stringify({ type: 'pong', timestamp: Date.now() }));
193
+ }
194
+ } catch (error) {
195
+ logToFile(`❌ Error handling dashboard message: ${error.message}`);
196
+ }
197
+ });
198
+ });
199
+
200
+ } else if (role === 'discovery') {
154
201
  // --- Agent Discovery Connection ---
155
202
  logToFile('📍 Discovery client connected');
156
203
 
@@ -169,11 +216,16 @@ wss.on('connection', (ws, req) => {
169
216
  logToFile(`🖥️ Agent connected: ${agentId}`);
170
217
  ws.agentId = agentId;
171
218
  agents.set(agentId, ws);
219
+
220
+ // Notify all dashboards of new agent
221
+ notifyAgentConnected(agentId);
172
222
 
173
223
  ws.on('close', () => {
174
224
  logToFile(`🔌 Agent disconnected: ${agentId}`);
175
225
  agents.delete(agentId);
176
- // Notify client if in a session
226
+
227
+ // Notify all dashboards of agent disconnection
228
+ notifyAgentDisconnected(agentId);
177
229
  });
178
230
 
179
231
  ws.on('message', (message) => handleSignalingMessage(ws, message, 'agent'));
@@ -220,6 +272,39 @@ wss.on('connection', (ws, req) => {
220
272
  }
221
273
  });
222
274
 
275
+ // --- Dashboard Broadcast Functions ---
276
+ function broadcastToDashboards(message) {
277
+ const messageStr = JSON.stringify(message);
278
+ dashboards.forEach((dashboardWs, userId) => {
279
+ if (dashboardWs.readyState === WebSocket.OPEN) {
280
+ dashboardWs.send(messageStr);
281
+ logToFile(`📡 Broadcasted to dashboard ${userId}: ${message.type}`);
282
+ } else {
283
+ // Clean up closed connections
284
+ dashboards.delete(userId);
285
+ logToFile(`🔌 Removed closed dashboard connection: ${userId}`);
286
+ }
287
+ });
288
+ }
289
+
290
+ function notifyAgentConnected(agentId) {
291
+ broadcastToDashboards({
292
+ type: 'agent-connected',
293
+ agentId: agentId,
294
+ timestamp: Date.now()
295
+ });
296
+ logToFile(`📢 Notified dashboards of agent connection: ${agentId}`);
297
+ }
298
+
299
+ function notifyAgentDisconnected(agentId) {
300
+ broadcastToDashboards({
301
+ type: 'agent-disconnected',
302
+ agentId: agentId,
303
+ timestamp: Date.now()
304
+ });
305
+ logToFile(`📢 Notified dashboards of agent disconnection: ${agentId}`);
306
+ }
307
+
223
308
  function handleSignalingMessage(ws, rawMessage, senderRole) {
224
309
  try {
225
310
  const message = JSON.parse(rawMessage);