shell-mirror 1.5.92 → 1.5.93
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 +27 -0
- package/public/app/dashboard.js +75 -8
- package/public/app/terminal.html +39 -6
- package/public/app/terminal.js +67 -6
package/package.json
CHANGED
package/public/app/dashboard.css
CHANGED
|
@@ -521,6 +521,33 @@ body {
|
|
|
521
521
|
text-align: center;
|
|
522
522
|
}
|
|
523
523
|
|
|
524
|
+
.agent-name-row {
|
|
525
|
+
display: flex;
|
|
526
|
+
align-items: center;
|
|
527
|
+
gap: 8px;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.btn-delete-agent {
|
|
531
|
+
background: none;
|
|
532
|
+
border: none;
|
|
533
|
+
cursor: pointer;
|
|
534
|
+
font-size: 0.9rem;
|
|
535
|
+
opacity: 0.4;
|
|
536
|
+
transition: opacity 0.2s ease;
|
|
537
|
+
padding: 2px 4px;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
.btn-delete-agent:hover {
|
|
541
|
+
opacity: 1;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
.agent-session-count {
|
|
545
|
+
font-weight: normal;
|
|
546
|
+
font-size: 0.7rem;
|
|
547
|
+
color: #666;
|
|
548
|
+
margin-left: 6px;
|
|
549
|
+
}
|
|
550
|
+
|
|
524
551
|
.agent-actions {
|
|
525
552
|
display: flex;
|
|
526
553
|
gap: 8px;
|
package/public/app/dashboard.js
CHANGED
|
@@ -587,9 +587,15 @@ class ShellMirrorDashboard {
|
|
|
587
587
|
<div class="agent-item ${!isConnectable ? 'agent-offline' : ''}">
|
|
588
588
|
<div class="agent-header">
|
|
589
589
|
<div class="agent-info">
|
|
590
|
-
<div class="agent-name"
|
|
590
|
+
<div class="agent-name-row">
|
|
591
|
+
<span class="agent-name">${agent.machineName || agent.agentId}</span>
|
|
592
|
+
<button class="btn-delete-agent" onclick="dashboard.deleteAgent('${agent.agentId}')" title="Remove agent">
|
|
593
|
+
🗑️
|
|
594
|
+
</button>
|
|
595
|
+
</div>
|
|
591
596
|
<div class="agent-status ${agent.status}">
|
|
592
597
|
${statusIcon} ${statusText}
|
|
598
|
+
${sessionCount > 0 ? `<span class="agent-session-count">(${sessionCount} session${sessionCount !== 1 ? 's' : ''})</span>` : ''}
|
|
593
599
|
</div>
|
|
594
600
|
<div class="agent-last-seen">Last seen: ${lastSeenText}</div>
|
|
595
601
|
</div>
|
|
@@ -597,7 +603,7 @@ class ShellMirrorDashboard {
|
|
|
597
603
|
${isConnectable ? `
|
|
598
604
|
<div class="agent-sessions-inline">
|
|
599
605
|
${sessionCount > 0 ? `
|
|
600
|
-
<div class="sessions-label">Sessions
|
|
606
|
+
<div class="sessions-label">Active Sessions</div>
|
|
601
607
|
<div class="sessions-list">
|
|
602
608
|
${sessionsHtml}
|
|
603
609
|
</div>
|
|
@@ -1033,6 +1039,10 @@ class ShellMirrorDashboard {
|
|
|
1033
1039
|
}
|
|
1034
1040
|
|
|
1035
1041
|
async connectToSession(agentId, sessionId) {
|
|
1042
|
+
// Validate agent is reachable before connecting
|
|
1043
|
+
const isReachable = await this.validateAgentBeforeConnect(agentId);
|
|
1044
|
+
if (!isReachable) return;
|
|
1045
|
+
|
|
1036
1046
|
// Track specific session connection in Google Analytics
|
|
1037
1047
|
if (typeof sendGAEvent === 'function') {
|
|
1038
1048
|
sendGAEvent('terminal_connect', {
|
|
@@ -1042,14 +1052,18 @@ class ShellMirrorDashboard {
|
|
|
1042
1052
|
session_id: sessionId
|
|
1043
1053
|
});
|
|
1044
1054
|
}
|
|
1045
|
-
|
|
1046
|
-
|
|
1055
|
+
|
|
1056
|
+
// Open in new browser tab so user can return to dashboard
|
|
1057
|
+
window.open(`/app/terminal.html?agent=${agentId}&session=${sessionId}`, '_blank');
|
|
1047
1058
|
}
|
|
1048
1059
|
|
|
1049
1060
|
async createNewSession(agentId) {
|
|
1050
|
-
//
|
|
1061
|
+
// Validate agent is reachable before connecting
|
|
1062
|
+
const isReachable = await this.validateAgentBeforeConnect(agentId);
|
|
1063
|
+
if (!isReachable) return;
|
|
1064
|
+
|
|
1051
1065
|
console.log(`[DASHBOARD] Creating new session for agent: ${agentId}`);
|
|
1052
|
-
|
|
1066
|
+
|
|
1053
1067
|
// Track explicit new session creation in Google Analytics
|
|
1054
1068
|
if (typeof sendGAEvent === 'function') {
|
|
1055
1069
|
sendGAEvent('terminal_connect', {
|
|
@@ -1058,8 +1072,61 @@ class ShellMirrorDashboard {
|
|
|
1058
1072
|
agent_id: agentId
|
|
1059
1073
|
});
|
|
1060
1074
|
}
|
|
1061
|
-
|
|
1062
|
-
|
|
1075
|
+
|
|
1076
|
+
// Open in new browser tab so user can return to dashboard
|
|
1077
|
+
window.open(`/app/terminal.html?agent=${agentId}&newSession=true`, '_blank');
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
async validateAgentBeforeConnect(agentId) {
|
|
1081
|
+
const agent = this.agents.find(a => a.agentId === agentId);
|
|
1082
|
+
if (!agent) {
|
|
1083
|
+
alert('Agent not found. Please refresh and try again.');
|
|
1084
|
+
return false;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
// Always test connectivity before connecting (not just for 'recent')
|
|
1088
|
+
console.log('[DASHBOARD] 🔍 Validating agent connectivity...');
|
|
1089
|
+
this.showAgentConnectionTest(agentId, 'testing');
|
|
1090
|
+
|
|
1091
|
+
const isReachable = await this.testAgentConnectivity(agentId);
|
|
1092
|
+
this.showAgentConnectionTest(agentId, 'done');
|
|
1093
|
+
|
|
1094
|
+
if (!isReachable) {
|
|
1095
|
+
this.showConnectionError(agent, 'Agent is not reachable. It may be offline or disconnected.');
|
|
1096
|
+
await this.refreshDashboardData();
|
|
1097
|
+
return false;
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return true;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
async deleteAgent(agentId) {
|
|
1104
|
+
const agent = this.agents.find(a => a.agentId === agentId);
|
|
1105
|
+
const agentName = agent ? (agent.machineName || agentId) : agentId;
|
|
1106
|
+
|
|
1107
|
+
if (!confirm(`Are you sure you want to remove "${agentName}" from the dashboard?\n\nThis will unregister the agent. If it's still running, it will re-register on next heartbeat.`)) {
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
try {
|
|
1112
|
+
const response = await fetch('/php-backend/api/delete-agent.php', {
|
|
1113
|
+
method: 'POST',
|
|
1114
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1115
|
+
credentials: 'include',
|
|
1116
|
+
body: JSON.stringify({ agentId })
|
|
1117
|
+
});
|
|
1118
|
+
|
|
1119
|
+
const data = await response.json();
|
|
1120
|
+
if (data.success) {
|
|
1121
|
+
console.log('[DASHBOARD] Agent deleted:', agentId);
|
|
1122
|
+
await this.refreshDashboardData();
|
|
1123
|
+
} else {
|
|
1124
|
+
alert('Failed to remove agent: ' + (data.message || 'Unknown error'));
|
|
1125
|
+
}
|
|
1126
|
+
} catch (error) {
|
|
1127
|
+
console.error('[DASHBOARD] Delete agent failed:', error);
|
|
1128
|
+
alert('Failed to remove agent: ' + error.message);
|
|
1129
|
+
}
|
|
1063
1130
|
}
|
|
1064
1131
|
|
|
1065
1132
|
startNewSession() {
|
package/public/app/terminal.html
CHANGED
|
@@ -153,17 +153,17 @@
|
|
|
153
153
|
.session-tab {
|
|
154
154
|
display: flex;
|
|
155
155
|
align-items: center;
|
|
156
|
-
gap:
|
|
157
|
-
padding: 8px
|
|
156
|
+
gap: 4px;
|
|
157
|
+
padding: 6px 8px;
|
|
158
158
|
background: transparent;
|
|
159
159
|
border: none;
|
|
160
160
|
border-bottom: 3px solid transparent;
|
|
161
|
-
color: #
|
|
162
|
-
|
|
163
|
-
min-width: 120px;
|
|
161
|
+
color: #888;
|
|
162
|
+
min-width: 100px;
|
|
164
163
|
white-space: nowrap;
|
|
165
164
|
transition: all 0.2s ease;
|
|
166
165
|
flex-shrink: 0;
|
|
166
|
+
border-radius: 6px 6px 0 0;
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
.session-tab:hover {
|
|
@@ -175,7 +175,40 @@
|
|
|
175
175
|
color: #fff;
|
|
176
176
|
font-weight: 600;
|
|
177
177
|
border-bottom-color: #667eea;
|
|
178
|
-
background: #
|
|
178
|
+
background: #333;
|
|
179
|
+
box-shadow: inset 0 -3px 0 #667eea;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
.session-tab-btn {
|
|
183
|
+
background: none;
|
|
184
|
+
border: none;
|
|
185
|
+
color: inherit;
|
|
186
|
+
font: inherit;
|
|
187
|
+
cursor: pointer;
|
|
188
|
+
padding: 2px 4px;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
.session-tab-close {
|
|
192
|
+
background: none;
|
|
193
|
+
border: none;
|
|
194
|
+
color: #666;
|
|
195
|
+
font-size: 1.1rem;
|
|
196
|
+
cursor: pointer;
|
|
197
|
+
padding: 0 4px;
|
|
198
|
+
line-height: 1;
|
|
199
|
+
border-radius: 3px;
|
|
200
|
+
opacity: 0;
|
|
201
|
+
transition: all 0.2s ease;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.session-tab:hover .session-tab-close {
|
|
205
|
+
opacity: 0.7;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
.session-tab-close:hover {
|
|
209
|
+
opacity: 1 !important;
|
|
210
|
+
background: rgba(255, 100, 100, 0.3);
|
|
211
|
+
color: #ff6b6b;
|
|
179
212
|
}
|
|
180
213
|
|
|
181
214
|
.session-tab-name {
|
package/public/app/terminal.js
CHANGED
|
@@ -411,6 +411,9 @@ function setupDirectConnection(directWs) {
|
|
|
411
411
|
term.clear();
|
|
412
412
|
term.write(`\r\n\x1b[36m✨ New session created: ${currentSession.name}\x1b[0m\r\n\r\n`);
|
|
413
413
|
|
|
414
|
+
// Update URL with session ID so refresh reconnects to same session
|
|
415
|
+
updateUrlWithSession(data.sessionId);
|
|
416
|
+
|
|
414
417
|
// Update UI
|
|
415
418
|
updateSessionDisplay();
|
|
416
419
|
|
|
@@ -995,12 +998,14 @@ function renderTabs() {
|
|
|
995
998
|
const displayName = session.name || 'Terminal Session';
|
|
996
999
|
|
|
997
1000
|
return `
|
|
998
|
-
<
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1001
|
+
<div class="session-tab ${isActive ? 'active' : ''}" title="${displayName}">
|
|
1002
|
+
<button class="session-tab-btn"
|
|
1003
|
+
onclick="switchToSession('${session.id}')"
|
|
1004
|
+
${isActive ? '' : ''}>
|
|
1005
|
+
<span class="session-tab-name">${displayName}</span>
|
|
1006
|
+
</button>
|
|
1007
|
+
<button class="session-tab-close" onclick="closeSession('${session.id}', event)" title="Close session">×</button>
|
|
1008
|
+
</div>
|
|
1004
1009
|
`;
|
|
1005
1010
|
}).join('');
|
|
1006
1011
|
}
|
|
@@ -1012,6 +1017,56 @@ function renderTabs() {
|
|
|
1012
1017
|
console.log('[CLIENT] ✅ Tabs rendered:', sessionsToRender.length, 'tabs');
|
|
1013
1018
|
}
|
|
1014
1019
|
|
|
1020
|
+
// Update URL with current session ID so refresh reconnects
|
|
1021
|
+
function updateUrlWithSession(sessionId) {
|
|
1022
|
+
const url = new URL(window.location.href);
|
|
1023
|
+
url.searchParams.set('session', sessionId);
|
|
1024
|
+
window.history.replaceState({}, '', url.toString());
|
|
1025
|
+
console.log('[CLIENT] 📍 URL updated with session:', sessionId);
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// Close a session with confirmation
|
|
1029
|
+
function closeSession(sessionId, event) {
|
|
1030
|
+
event.stopPropagation(); // Don't trigger tab switch
|
|
1031
|
+
|
|
1032
|
+
const session = availableSessions.find(s => s.id === sessionId);
|
|
1033
|
+
const sessionName = session?.name || 'this session';
|
|
1034
|
+
|
|
1035
|
+
if (!confirm(`Close "${sessionName}"?\n\nThis will terminate the terminal session.`)) {
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
console.log('[CLIENT] 🗑️ Closing session:', sessionId);
|
|
1040
|
+
|
|
1041
|
+
// Send close request to agent
|
|
1042
|
+
const closeMessage = {
|
|
1043
|
+
type: 'close_session',
|
|
1044
|
+
sessionId: sessionId
|
|
1045
|
+
};
|
|
1046
|
+
|
|
1047
|
+
if (dataChannel && dataChannel.readyState === 'open') {
|
|
1048
|
+
dataChannel.send(JSON.stringify(closeMessage));
|
|
1049
|
+
} else if (ws && ws.readyState === WebSocket.OPEN) {
|
|
1050
|
+
ws.send(JSON.stringify(closeMessage));
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
// Remove from available sessions
|
|
1054
|
+
availableSessions = availableSessions.filter(s => s.id !== sessionId);
|
|
1055
|
+
|
|
1056
|
+
// If closing current session, switch to another or show message
|
|
1057
|
+
if (currentSession && currentSession.id === sessionId) {
|
|
1058
|
+
if (availableSessions.length > 0) {
|
|
1059
|
+
switchToSession(availableSessions[0].id);
|
|
1060
|
+
} else {
|
|
1061
|
+
currentSession = null;
|
|
1062
|
+
term.clear();
|
|
1063
|
+
term.write('\r\n\x1b[33mSession closed. Click + to create a new session.\x1b[0m\r\n');
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
renderTabs();
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1015
1070
|
function getConnectionStatus() {
|
|
1016
1071
|
// Check direct WebSocket connection
|
|
1017
1072
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -1119,6 +1174,9 @@ function handleSessionMessage(message) {
|
|
|
1119
1174
|
term.clear();
|
|
1120
1175
|
term.write(`\r\n\x1b[36m✨ New session created: ${currentSession.name}\x1b[0m\r\n\r\n`);
|
|
1121
1176
|
|
|
1177
|
+
// Update URL with session ID so refresh reconnects to same session
|
|
1178
|
+
updateUrlWithSession(message.sessionId);
|
|
1179
|
+
|
|
1122
1180
|
// Update UI
|
|
1123
1181
|
updateSessionDisplay();
|
|
1124
1182
|
|
|
@@ -1135,6 +1193,9 @@ function handleSessionMessage(message) {
|
|
|
1135
1193
|
term.clear(); // Clear terminal for new session
|
|
1136
1194
|
console.log('[CLIENT] ✅ Switched to session:', currentSession);
|
|
1137
1195
|
|
|
1196
|
+
// Update URL so refresh reconnects to this session
|
|
1197
|
+
updateUrlWithSession(message.sessionId);
|
|
1198
|
+
|
|
1138
1199
|
// Save updated session info
|
|
1139
1200
|
saveSessionToLocalStorage(AGENT_ID, currentSession);
|
|
1140
1201
|
break;
|