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 +1 -1
- package/public/app/dashboard.css +158 -0
- package/public/app/dashboard.html +4 -2
- package/public/app/dashboard.js +335 -10
- package/server.js +87 -2
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,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="
|
|
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,285 @@ 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
|
-
// 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
|
-
},
|
|
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
|
-
|
|
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 === '
|
|
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
|
-
|
|
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);
|