shell-mirror 1.5.56 → 1.5.58
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 +56 -0
- package/public/app/dashboard.js +159 -21
package/package.json
CHANGED
package/public/app/dashboard.css
CHANGED
|
@@ -152,6 +152,37 @@ body {
|
|
|
152
152
|
animation: spin 1s linear infinite;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
/* Inline Refresh Button */
|
|
156
|
+
.refresh-btn-inline {
|
|
157
|
+
background: #f8f9fa;
|
|
158
|
+
border: 1px solid #e9ecef;
|
|
159
|
+
border-radius: 50%;
|
|
160
|
+
color: #666;
|
|
161
|
+
width: 32px;
|
|
162
|
+
height: 32px;
|
|
163
|
+
cursor: pointer;
|
|
164
|
+
transition: all 0.2s ease;
|
|
165
|
+
display: flex;
|
|
166
|
+
align-items: center;
|
|
167
|
+
justify-content: center;
|
|
168
|
+
font-size: 0.9rem;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
.refresh-btn-inline:hover:not(:disabled) {
|
|
172
|
+
background: #e9ecef;
|
|
173
|
+
color: #333;
|
|
174
|
+
transform: scale(1.05);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
.refresh-btn-inline.loading {
|
|
178
|
+
opacity: 0.7;
|
|
179
|
+
cursor: not-allowed;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.refresh-btn-inline.loading .refresh-icon {
|
|
183
|
+
animation: spin 1s linear infinite;
|
|
184
|
+
}
|
|
185
|
+
|
|
155
186
|
.connection-status {
|
|
156
187
|
font-size: 0.8rem;
|
|
157
188
|
font-weight: 500;
|
|
@@ -242,6 +273,12 @@ body {
|
|
|
242
273
|
margin-bottom: 20px;
|
|
243
274
|
}
|
|
244
275
|
|
|
276
|
+
.card-title-section {
|
|
277
|
+
display: flex;
|
|
278
|
+
align-items: center;
|
|
279
|
+
gap: 12px;
|
|
280
|
+
}
|
|
281
|
+
|
|
245
282
|
.card-header h2 {
|
|
246
283
|
font-size: 1.3rem;
|
|
247
284
|
font-weight: 600;
|
|
@@ -833,4 +870,23 @@ body {
|
|
|
833
870
|
opacity: 0;
|
|
834
871
|
transform: translateX(100%);
|
|
835
872
|
}
|
|
873
|
+
}
|
|
874
|
+
|
|
875
|
+
/* API Error Display */
|
|
876
|
+
.api-error {
|
|
877
|
+
text-align: center;
|
|
878
|
+
padding: 20px;
|
|
879
|
+
background: #fff5f5;
|
|
880
|
+
border: 1px solid #fed7d7;
|
|
881
|
+
border-radius: 8px;
|
|
882
|
+
color: #c53030;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
.api-error p {
|
|
886
|
+
margin-bottom: 15px;
|
|
887
|
+
font-weight: 500;
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
.api-error button {
|
|
891
|
+
margin-top: 10px;
|
|
836
892
|
}
|
package/public/app/dashboard.js
CHANGED
|
@@ -12,6 +12,8 @@ class ShellMirrorDashboard {
|
|
|
12
12
|
this.refreshInterval = null;
|
|
13
13
|
this.lastRefresh = null;
|
|
14
14
|
this.isRefreshing = false;
|
|
15
|
+
this.connectionStatusDebounce = null;
|
|
16
|
+
this.currentConnectionStatus = null;
|
|
15
17
|
this.init();
|
|
16
18
|
}
|
|
17
19
|
|
|
@@ -102,8 +104,24 @@ class ShellMirrorDashboard {
|
|
|
102
104
|
}
|
|
103
105
|
|
|
104
106
|
setupWebSocket() {
|
|
107
|
+
// Detect production environment
|
|
108
|
+
const isProduction = window.location.hostname === 'shellmirror.app' ||
|
|
109
|
+
window.location.hostname === 'www.shellmirror.app' ||
|
|
110
|
+
window.location.hostname === 'www.igori.eu' ||
|
|
111
|
+
window.location.hostname === 'igori.eu';
|
|
112
|
+
|
|
105
113
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
106
|
-
|
|
114
|
+
let wsUrl;
|
|
115
|
+
|
|
116
|
+
if (isProduction) {
|
|
117
|
+
// For production, try the Heroku WebSocket app URL
|
|
118
|
+
// This may need to be adjusted based on actual deployment architecture
|
|
119
|
+
wsUrl = `wss://shell-mirror-30aa5479ceaf.herokuapp.com/?role=dashboard`;
|
|
120
|
+
console.log('[DASHBOARD] 🌐 Production environment detected, using Heroku WebSocket URL');
|
|
121
|
+
} else {
|
|
122
|
+
// For development, use same host
|
|
123
|
+
wsUrl = `${protocol}//${window.location.host}/?role=dashboard`;
|
|
124
|
+
}
|
|
107
125
|
|
|
108
126
|
console.log('[DASHBOARD] 🔌 Connecting to WebSocket:', wsUrl);
|
|
109
127
|
|
|
@@ -111,9 +129,20 @@ class ShellMirrorDashboard {
|
|
|
111
129
|
this.websocket = new WebSocket(wsUrl);
|
|
112
130
|
|
|
113
131
|
this.websocket.onopen = () => {
|
|
114
|
-
console.log('[DASHBOARD] ✅ WebSocket connected');
|
|
132
|
+
console.log('[DASHBOARD] ✅ WebSocket connected to:', wsUrl);
|
|
115
133
|
this.reconnectAttempts = 0;
|
|
116
134
|
this.updateConnectionStatus('connected');
|
|
135
|
+
|
|
136
|
+
// Send authentication info if available
|
|
137
|
+
const user = this.user;
|
|
138
|
+
if (user) {
|
|
139
|
+
console.log('[DASHBOARD] 🔐 Sending authentication to WebSocket');
|
|
140
|
+
this.websocket.send(JSON.stringify({
|
|
141
|
+
type: 'authenticate',
|
|
142
|
+
userId: user.id || user.email,
|
|
143
|
+
email: user.email
|
|
144
|
+
}));
|
|
145
|
+
}
|
|
117
146
|
};
|
|
118
147
|
|
|
119
148
|
this.websocket.onmessage = (event) => {
|
|
@@ -121,13 +150,38 @@ class ShellMirrorDashboard {
|
|
|
121
150
|
};
|
|
122
151
|
|
|
123
152
|
this.websocket.onclose = (event) => {
|
|
124
|
-
|
|
153
|
+
const closeReasons = {
|
|
154
|
+
1000: 'Normal Closure',
|
|
155
|
+
1001: 'Going Away',
|
|
156
|
+
1002: 'Protocol Error',
|
|
157
|
+
1003: 'Unsupported Data',
|
|
158
|
+
1004: 'Reserved',
|
|
159
|
+
1005: 'No Status Rcvd',
|
|
160
|
+
1006: 'Abnormal Closure',
|
|
161
|
+
1007: 'Invalid frame payload data',
|
|
162
|
+
1008: 'Policy Violation',
|
|
163
|
+
1009: 'Message too big',
|
|
164
|
+
1010: 'Mandatory Extension',
|
|
165
|
+
1011: 'Internal Server Error',
|
|
166
|
+
1015: 'TLS Handshake'
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
const reason = closeReasons[event.code] || 'Unknown';
|
|
170
|
+
console.log(`[DASHBOARD] 🔌 WebSocket closed: ${event.code} (${reason})`, event.reason);
|
|
171
|
+
|
|
172
|
+
if (event.code === 1008) {
|
|
173
|
+
console.error('[DASHBOARD] ❌ Authentication required - WebSocket rejected connection');
|
|
174
|
+
} else if (event.code === 1006) {
|
|
175
|
+
console.error('[DASHBOARD] ❌ Abnormal closure - WebSocket endpoint may not exist');
|
|
176
|
+
}
|
|
177
|
+
|
|
125
178
|
this.updateConnectionStatus('disconnected');
|
|
126
179
|
this.attemptReconnect();
|
|
127
180
|
};
|
|
128
181
|
|
|
129
182
|
this.websocket.onerror = (error) => {
|
|
130
183
|
console.error('[DASHBOARD] ❌ WebSocket error:', error);
|
|
184
|
+
console.log('[DASHBOARD] 🔍 WebSocket URL attempted:', wsUrl);
|
|
131
185
|
this.updateConnectionStatus('error');
|
|
132
186
|
};
|
|
133
187
|
|
|
@@ -222,22 +276,74 @@ class ShellMirrorDashboard {
|
|
|
222
276
|
this.setupWebSocket();
|
|
223
277
|
}, delay);
|
|
224
278
|
} else {
|
|
225
|
-
console.log('[DASHBOARD] ❌ Max reconnection attempts reached
|
|
226
|
-
this.updateConnectionStatus('
|
|
279
|
+
console.log('[DASHBOARD] ❌ Max reconnection attempts reached, switching to HTTP-only mode');
|
|
280
|
+
this.updateConnectionStatus('offline');
|
|
281
|
+
this.enableHttpOnlyMode();
|
|
227
282
|
}
|
|
228
283
|
}
|
|
229
284
|
|
|
285
|
+
enableHttpOnlyMode() {
|
|
286
|
+
console.log('[DASHBOARD] 📡 Enabling HTTP-only mode (no real-time updates)');
|
|
287
|
+
this.websocket = null;
|
|
288
|
+
|
|
289
|
+
// Update UI to show HTTP-only mode
|
|
290
|
+
const connectionStatus = document.getElementById('connection-status');
|
|
291
|
+
if (connectionStatus) {
|
|
292
|
+
connectionStatus.textContent = '📡 HTTP Only';
|
|
293
|
+
connectionStatus.className = 'connection-status offline';
|
|
294
|
+
connectionStatus.title = 'Real-time updates unavailable - using polling';
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Continue with HTTP polling only
|
|
298
|
+
console.log('[DASHBOARD] ✅ Dashboard running in HTTP-only mode');
|
|
299
|
+
}
|
|
300
|
+
|
|
230
301
|
updateConnectionStatus(status) {
|
|
302
|
+
// Debounce rapid status changes to prevent UI flickering
|
|
303
|
+
if (this.connectionStatusDebounce) {
|
|
304
|
+
clearTimeout(this.connectionStatusDebounce);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// Don't update if status is the same
|
|
308
|
+
if (this.currentConnectionStatus === status) {
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// For rapid disconnection/reconnection, only show status after a delay
|
|
313
|
+
if (status === 'disconnected' && this.currentConnectionStatus === 'connected') {
|
|
314
|
+
this.connectionStatusDebounce = setTimeout(() => {
|
|
315
|
+
this.setConnectionStatusUI(status);
|
|
316
|
+
}, 2000); // Wait 2 seconds before showing disconnected
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// For reconnection, show immediately but don't flicker
|
|
321
|
+
if (status === 'connected' && this.connectionStatusDebounce) {
|
|
322
|
+
clearTimeout(this.connectionStatusDebounce);
|
|
323
|
+
this.connectionStatusDebounce = null;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
this.setConnectionStatusUI(status);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
setConnectionStatusUI(status) {
|
|
330
|
+
this.currentConnectionStatus = status;
|
|
231
331
|
const connectionStatus = document.getElementById('connection-status');
|
|
232
332
|
if (connectionStatus) {
|
|
233
333
|
connectionStatus.className = `connection-status ${status}`;
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
334
|
+
|
|
335
|
+
// Only show status when there are issues or special modes
|
|
336
|
+
const statusConfig = {
|
|
337
|
+
connected: { text: '', show: false }, // Hide when everything is working
|
|
338
|
+
disconnected: { text: '🟡 Reconnecting...', show: true },
|
|
339
|
+
error: { text: '🔴 Connection Error', show: true },
|
|
340
|
+
failed: { text: '🔴 Offline', show: true },
|
|
341
|
+
offline: { text: '📡 HTTP Only', show: true }
|
|
239
342
|
};
|
|
240
|
-
|
|
343
|
+
|
|
344
|
+
const config = statusConfig[status] || { text: '❓ Unknown', show: true };
|
|
345
|
+
connectionStatus.textContent = config.text;
|
|
346
|
+
connectionStatus.style.display = config.show ? 'inline-block' : 'none';
|
|
241
347
|
}
|
|
242
348
|
}
|
|
243
349
|
|
|
@@ -301,15 +407,27 @@ class ShellMirrorDashboard {
|
|
|
301
407
|
|
|
302
408
|
async loadDashboardData() {
|
|
303
409
|
try {
|
|
304
|
-
// Load active agents
|
|
305
|
-
|
|
410
|
+
// Load active agents with detailed debugging
|
|
411
|
+
console.log('[DASHBOARD] 📡 Fetching agents from API...');
|
|
412
|
+
const agentsResponse = await fetch('/php-backend/api/agents-list.php', {
|
|
413
|
+
credentials: 'include' // Include authentication cookies
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
console.log('[DASHBOARD] 🔍 API Response Status:', agentsResponse.status);
|
|
417
|
+
console.log('[DASHBOARD] 🔍 API Response Headers:', Object.fromEntries(agentsResponse.headers.entries()));
|
|
418
|
+
|
|
306
419
|
const agentsData = await agentsResponse.json();
|
|
420
|
+
console.log('[DASHBOARD] 🔍 API Response Data:', agentsData);
|
|
307
421
|
|
|
308
422
|
if (agentsData.success && agentsData.data && agentsData.data.agents) {
|
|
309
423
|
this.agents = agentsData.data.agents;
|
|
424
|
+
console.log('[DASHBOARD] ✅ Loaded agents:', this.agents.length);
|
|
310
425
|
|
|
311
426
|
// Load session data from localStorage (persisted from terminal connections)
|
|
312
427
|
this.loadSessionsFromStorage();
|
|
428
|
+
} else {
|
|
429
|
+
console.warn('[DASHBOARD] ⚠️ No agents found in API response:', agentsData);
|
|
430
|
+
this.agents = [];
|
|
313
431
|
}
|
|
314
432
|
|
|
315
433
|
// TODO: Load session history when API is available
|
|
@@ -331,7 +449,25 @@ class ShellMirrorDashboard {
|
|
|
331
449
|
];
|
|
332
450
|
|
|
333
451
|
} catch (error) {
|
|
334
|
-
console.error('Failed to load dashboard data:', error);
|
|
452
|
+
console.error('[DASHBOARD] ❌ Failed to load dashboard data:', error);
|
|
453
|
+
this.agents = [];
|
|
454
|
+
|
|
455
|
+
// Show error in UI
|
|
456
|
+
const agentsCard = document.querySelector('.dashboard-card');
|
|
457
|
+
if (agentsCard) {
|
|
458
|
+
agentsCard.innerHTML = `
|
|
459
|
+
<div class="card-header">
|
|
460
|
+
<h2>🖥️ Active Agents</h2>
|
|
461
|
+
<span class="agent-count">Error</span>
|
|
462
|
+
</div>
|
|
463
|
+
<div class="card-content">
|
|
464
|
+
<div class="api-error">
|
|
465
|
+
<p>⚠️ Failed to load agents: ${error.message}</p>
|
|
466
|
+
<button onclick="dashboard.manualRefresh()" class="btn-primary">Retry</button>
|
|
467
|
+
</div>
|
|
468
|
+
</div>
|
|
469
|
+
`;
|
|
470
|
+
}
|
|
335
471
|
}
|
|
336
472
|
}
|
|
337
473
|
|
|
@@ -366,14 +502,11 @@ class ShellMirrorDashboard {
|
|
|
366
502
|
}
|
|
367
503
|
|
|
368
504
|
renderAuthenticatedDashboard() {
|
|
369
|
-
// Update user section
|
|
505
|
+
// Update user section - simplified without refresh button
|
|
370
506
|
document.getElementById('user-section').innerHTML = `
|
|
371
507
|
<div class="dashboard-controls">
|
|
372
|
-
<span id="connection-status" class="connection-status"
|
|
508
|
+
<span id="connection-status" class="connection-status" style="display: none;"></span>
|
|
373
509
|
<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
510
|
</div>
|
|
378
511
|
<div class="user-info">
|
|
379
512
|
<span class="user-name">${this.user.name || this.user.email}</span>
|
|
@@ -438,8 +571,13 @@ class ShellMirrorDashboard {
|
|
|
438
571
|
return `
|
|
439
572
|
<div class="dashboard-card">
|
|
440
573
|
<div class="card-header">
|
|
441
|
-
<
|
|
442
|
-
|
|
574
|
+
<div class="card-title-section">
|
|
575
|
+
<h2>🖥️ Active Agents</h2>
|
|
576
|
+
<span class="agent-count">${agentCount} agent${agentCount !== 1 ? 's' : ''}</span>
|
|
577
|
+
</div>
|
|
578
|
+
<button id="refresh-btn" class="refresh-btn-inline" onclick="dashboard.manualRefresh()" title="Refresh agents">
|
|
579
|
+
<span class="refresh-icon">🔄</span>
|
|
580
|
+
</button>
|
|
443
581
|
</div>
|
|
444
582
|
<div class="card-content">
|
|
445
583
|
${agentCount > 0 ? agentsHtml : '<p class="no-data">No active agents. <a href="#" onclick="dashboard.showAgentInstructions()">Set up an agent</a></p>'}
|