shell-mirror 1.5.54 → 1.5.56
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 +1 -1
- package/public/app/dashboard.css +139 -0
- package/public/app/dashboard.html +4 -2
- package/public/app/dashboard.js +233 -7
- package/server.js +89 -7
package/package.json
CHANGED
package/public/app/dashboard.css
CHANGED
|
@@ -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,47 @@ 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
|
+
}
|
|
697
836
|
}
|
|
@@ -61,8 +61,10 @@
|
|
|
61
61
|
<h1>Shell Mirror</h1>
|
|
62
62
|
<span class="subtitle">Dashboard</span>
|
|
63
63
|
</div>
|
|
64
|
-
<div class="
|
|
65
|
-
|
|
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>
|
package/public/app/dashboard.js
CHANGED
|
@@ -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,216 @@ class ShellMirrorDashboard {
|
|
|
51
58
|
}
|
|
52
59
|
|
|
53
60
|
startAutoRefresh() {
|
|
54
|
-
//
|
|
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.
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
if (this.isAuthenticated && !this.isRefreshing) {
|
|
69
|
+
await this.refreshDashboardData();
|
|
70
|
+
}
|
|
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
|
+
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
106
|
+
const wsUrl = `${protocol}//${window.location.host}/?role=dashboard`;
|
|
107
|
+
|
|
108
|
+
console.log('[DASHBOARD] 🔌 Connecting to WebSocket:', wsUrl);
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
this.websocket = new WebSocket(wsUrl);
|
|
112
|
+
|
|
113
|
+
this.websocket.onopen = () => {
|
|
114
|
+
console.log('[DASHBOARD] ✅ WebSocket connected');
|
|
115
|
+
this.reconnectAttempts = 0;
|
|
116
|
+
this.updateConnectionStatus('connected');
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
this.websocket.onmessage = (event) => {
|
|
120
|
+
this.handleWebSocketMessage(event.data);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.websocket.onclose = (event) => {
|
|
124
|
+
console.log('[DASHBOARD] 🔌 WebSocket closed:', event.code, event.reason);
|
|
125
|
+
this.updateConnectionStatus('disconnected');
|
|
126
|
+
this.attemptReconnect();
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
this.websocket.onerror = (error) => {
|
|
130
|
+
console.error('[DASHBOARD] ❌ WebSocket error:', error);
|
|
131
|
+
this.updateConnectionStatus('error');
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// Send periodic ping to keep connection alive
|
|
135
|
+
setInterval(() => {
|
|
136
|
+
if (this.websocket && this.websocket.readyState === WebSocket.OPEN) {
|
|
137
|
+
this.websocket.send(JSON.stringify({ type: 'ping' }));
|
|
138
|
+
}
|
|
139
|
+
}, 30000);
|
|
140
|
+
|
|
141
|
+
} catch (error) {
|
|
142
|
+
console.error('[DASHBOARD] ❌ Failed to setup WebSocket:', error);
|
|
143
|
+
this.updateConnectionStatus('error');
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
handleWebSocketMessage(data) {
|
|
148
|
+
try {
|
|
149
|
+
const message = JSON.parse(data);
|
|
150
|
+
console.log('[DASHBOARD] 📨 WebSocket message:', message);
|
|
151
|
+
|
|
152
|
+
switch (message.type) {
|
|
153
|
+
case 'agent-list':
|
|
154
|
+
// Initial agent list or refresh
|
|
155
|
+
this.handleAgentListUpdate(message.agents);
|
|
156
|
+
break;
|
|
157
|
+
case 'agent-connected':
|
|
158
|
+
this.handleAgentConnected(message.agentId);
|
|
159
|
+
break;
|
|
160
|
+
case 'agent-disconnected':
|
|
161
|
+
this.handleAgentDisconnected(message.agentId);
|
|
162
|
+
break;
|
|
163
|
+
case 'pong':
|
|
164
|
+
console.log('[DASHBOARD] 🏓 Received pong');
|
|
165
|
+
break;
|
|
166
|
+
default:
|
|
167
|
+
console.log('[DASHBOARD] ❓ Unknown message type:', message.type);
|
|
168
|
+
}
|
|
169
|
+
} catch (error) {
|
|
170
|
+
console.error('[DASHBOARD] ❌ Error handling WebSocket message:', error);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
handleAgentListUpdate(agentIds) {
|
|
175
|
+
console.log('[DASHBOARD] 📋 Agent list update:', agentIds);
|
|
176
|
+
// This is just the initial list of agent IDs, we still need to load full data
|
|
177
|
+
this.refreshDashboardData();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
handleAgentConnected(agentId) {
|
|
181
|
+
console.log('[DASHBOARD] ✅ Agent connected:', agentId);
|
|
182
|
+
this.showAgentNotification(agentId, 'connected');
|
|
183
|
+
// Refresh data to get the new agent's details
|
|
184
|
+
this.refreshDashboardData();
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
handleAgentDisconnected(agentId) {
|
|
188
|
+
console.log('[DASHBOARD] ❌ Agent disconnected:', agentId);
|
|
189
|
+
this.showAgentNotification(agentId, 'disconnected');
|
|
190
|
+
// Refresh data to update agent status
|
|
191
|
+
this.refreshDashboardData();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
showAgentNotification(agentId, status) {
|
|
195
|
+
const message = status === 'connected'
|
|
196
|
+
? `🟢 Agent ${agentId} connected`
|
|
197
|
+
: `🔴 Agent ${agentId} disconnected`;
|
|
198
|
+
|
|
199
|
+
// Create notification element
|
|
200
|
+
const notification = document.createElement('div');
|
|
201
|
+
notification.className = `agent-notification ${status}`;
|
|
202
|
+
notification.textContent = message;
|
|
203
|
+
|
|
204
|
+
// Add to page
|
|
205
|
+
document.body.appendChild(notification);
|
|
206
|
+
|
|
207
|
+
// Auto-remove after 5 seconds
|
|
208
|
+
setTimeout(() => {
|
|
209
|
+
if (document.body.contains(notification)) {
|
|
210
|
+
document.body.removeChild(notification);
|
|
60
211
|
}
|
|
61
|
-
},
|
|
212
|
+
}, 5000);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
attemptReconnect() {
|
|
216
|
+
if (this.reconnectAttempts < this.maxReconnectAttempts && this.isAuthenticated) {
|
|
217
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts), 30000);
|
|
218
|
+
console.log(`[DASHBOARD] 🔄 Attempting reconnect in ${delay}ms (attempt ${this.reconnectAttempts + 1})`);
|
|
219
|
+
|
|
220
|
+
setTimeout(() => {
|
|
221
|
+
this.reconnectAttempts++;
|
|
222
|
+
this.setupWebSocket();
|
|
223
|
+
}, delay);
|
|
224
|
+
} else {
|
|
225
|
+
console.log('[DASHBOARD] ❌ Max reconnection attempts reached or not authenticated');
|
|
226
|
+
this.updateConnectionStatus('failed');
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
updateConnectionStatus(status) {
|
|
231
|
+
const connectionStatus = document.getElementById('connection-status');
|
|
232
|
+
if (connectionStatus) {
|
|
233
|
+
connectionStatus.className = `connection-status ${status}`;
|
|
234
|
+
const statusText = {
|
|
235
|
+
connected: '🟢 Live',
|
|
236
|
+
disconnected: '🟡 Reconnecting...',
|
|
237
|
+
error: '🔴 Connection Error',
|
|
238
|
+
failed: '🔴 Offline'
|
|
239
|
+
};
|
|
240
|
+
connectionStatus.textContent = statusText[status] || '❓ Unknown';
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async manualRefresh() {
|
|
245
|
+
if (this.isRefreshing) {
|
|
246
|
+
console.log('[DASHBOARD] ⚠️ Refresh already in progress, ignoring manual request');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
console.log('[DASHBOARD] 🔄 Manual refresh triggered');
|
|
251
|
+
|
|
252
|
+
// Show loading state on refresh button
|
|
253
|
+
const refreshBtn = document.getElementById('refresh-btn');
|
|
254
|
+
if (refreshBtn) {
|
|
255
|
+
refreshBtn.classList.add('loading');
|
|
256
|
+
refreshBtn.disabled = true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
try {
|
|
260
|
+
await this.refreshDashboardData();
|
|
261
|
+
console.log('[DASHBOARD] ✅ Manual refresh completed');
|
|
262
|
+
} catch (error) {
|
|
263
|
+
console.error('[DASHBOARD] ❌ Manual refresh failed:', error);
|
|
264
|
+
} finally {
|
|
265
|
+
// Remove loading state
|
|
266
|
+
if (refreshBtn) {
|
|
267
|
+
refreshBtn.classList.remove('loading');
|
|
268
|
+
refreshBtn.disabled = false;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
62
271
|
}
|
|
63
272
|
|
|
64
273
|
updateAgentsDisplay() {
|
|
@@ -157,8 +366,15 @@ class ShellMirrorDashboard {
|
|
|
157
366
|
}
|
|
158
367
|
|
|
159
368
|
renderAuthenticatedDashboard() {
|
|
160
|
-
// Update user section
|
|
369
|
+
// Update user section with refresh button and status
|
|
161
370
|
document.getElementById('user-section').innerHTML = `
|
|
371
|
+
<div class="dashboard-controls">
|
|
372
|
+
<span id="connection-status" class="connection-status">🟡 Connecting...</span>
|
|
373
|
+
<span id="refresh-status" class="refresh-status">Initializing...</span>
|
|
374
|
+
<button id="refresh-btn" class="refresh-btn" onclick="dashboard.manualRefresh()" title="Refresh agents">
|
|
375
|
+
<span class="refresh-icon">🔄</span>
|
|
376
|
+
</button>
|
|
377
|
+
</div>
|
|
162
378
|
<div class="user-info">
|
|
163
379
|
<span class="user-name">${this.user.name || this.user.email}</span>
|
|
164
380
|
<div class="user-dropdown">
|
|
@@ -669,4 +885,14 @@ function handleLogin() {
|
|
|
669
885
|
let dashboard;
|
|
670
886
|
document.addEventListener('DOMContentLoaded', () => {
|
|
671
887
|
dashboard = new ShellMirrorDashboard();
|
|
888
|
+
});
|
|
889
|
+
|
|
890
|
+
// Cleanup on page unload
|
|
891
|
+
window.addEventListener('beforeunload', () => {
|
|
892
|
+
if (dashboard && dashboard.websocket) {
|
|
893
|
+
dashboard.websocket.close();
|
|
894
|
+
}
|
|
895
|
+
if (dashboard && dashboard.refreshInterval) {
|
|
896
|
+
clearInterval(dashboard.refreshInterval);
|
|
897
|
+
}
|
|
672
898
|
});
|
package/server.js
CHANGED
|
@@ -28,16 +28,13 @@ const server = http.createServer(app);
|
|
|
28
28
|
const wss = new WebSocket.Server({ server });
|
|
29
29
|
|
|
30
30
|
// --- Session Management ---
|
|
31
|
-
const sessionLifetime = isProduction ? (30 * 24 * 60 * 60 * 1000) : (7 * 24 * 60 * 60 * 1000); // 30 days production, 7 days development
|
|
32
31
|
const sessionParser = session({
|
|
33
32
|
secret: process.env.SESSION_SECRET,
|
|
34
33
|
resave: false,
|
|
35
34
|
saveUninitialized: false,
|
|
36
35
|
cookie: {
|
|
37
|
-
secure:
|
|
38
|
-
maxAge:
|
|
39
|
-
httpOnly: true,
|
|
40
|
-
sameSite: 'lax'
|
|
36
|
+
secure: process.env.NODE_ENV === 'production',
|
|
37
|
+
maxAge: 30 * 24 * 60 * 60 * 1000 // 30 days in milliseconds
|
|
41
38
|
}
|
|
42
39
|
});
|
|
43
40
|
app.use(sessionParser);
|
|
@@ -47,6 +44,7 @@ app.use(passport.session());
|
|
|
47
44
|
// --- In-memory data stores ---
|
|
48
45
|
const agents = new Map(); // Stores connected agent sockets
|
|
49
46
|
const clients = new Map(); // Stores connected client (browser) sockets
|
|
47
|
+
const dashboards = new Map(); // Stores connected dashboard sockets
|
|
50
48
|
const sessions = new Map(); // Maps session IDs to agent/client pairs
|
|
51
49
|
|
|
52
50
|
// --- Environment Detection ---
|
|
@@ -153,7 +151,53 @@ wss.on('connection', (ws, req) => {
|
|
|
153
151
|
const role = params.get('role');
|
|
154
152
|
const agentId = params.get('agentId');
|
|
155
153
|
|
|
156
|
-
if (role === '
|
|
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') {
|
|
157
201
|
// --- Agent Discovery Connection ---
|
|
158
202
|
logToFile('📍 Discovery client connected');
|
|
159
203
|
|
|
@@ -172,11 +216,16 @@ wss.on('connection', (ws, req) => {
|
|
|
172
216
|
logToFile(`🖥️ Agent connected: ${agentId}`);
|
|
173
217
|
ws.agentId = agentId;
|
|
174
218
|
agents.set(agentId, ws);
|
|
219
|
+
|
|
220
|
+
// Notify all dashboards of new agent
|
|
221
|
+
notifyAgentConnected(agentId);
|
|
175
222
|
|
|
176
223
|
ws.on('close', () => {
|
|
177
224
|
logToFile(`🔌 Agent disconnected: ${agentId}`);
|
|
178
225
|
agents.delete(agentId);
|
|
179
|
-
|
|
226
|
+
|
|
227
|
+
// Notify all dashboards of agent disconnection
|
|
228
|
+
notifyAgentDisconnected(agentId);
|
|
180
229
|
});
|
|
181
230
|
|
|
182
231
|
ws.on('message', (message) => handleSignalingMessage(ws, message, 'agent'));
|
|
@@ -223,6 +272,39 @@ wss.on('connection', (ws, req) => {
|
|
|
223
272
|
}
|
|
224
273
|
});
|
|
225
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
|
+
|
|
226
308
|
function handleSignalingMessage(ws, rawMessage, senderRole) {
|
|
227
309
|
try {
|
|
228
310
|
const message = JSON.parse(rawMessage);
|