shell-mirror 1.5.57 โ†’ 1.5.59

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.57",
3
+ "version": "1.5.59",
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": {
@@ -152,6 +152,59 @@ 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
+
186
+ /* Cleanup Button */
187
+ .cleanup-btn-inline {
188
+ background: #ffebee;
189
+ border: 1px solid #f48fb1;
190
+ border-radius: 50%;
191
+ color: #d32f2f;
192
+ width: 32px;
193
+ height: 32px;
194
+ cursor: pointer;
195
+ transition: all 0.2s ease;
196
+ display: flex;
197
+ align-items: center;
198
+ justify-content: center;
199
+ font-size: 0.9rem;
200
+ }
201
+
202
+ .cleanup-btn-inline:hover:not(:disabled) {
203
+ background: #f8bbd9;
204
+ color: #b71c1c;
205
+ transform: scale(1.05);
206
+ }
207
+
155
208
  .connection-status {
156
209
  font-size: 0.8rem;
157
210
  font-weight: 500;
@@ -242,6 +295,18 @@ body {
242
295
  margin-bottom: 20px;
243
296
  }
244
297
 
298
+ .card-title-section {
299
+ display: flex;
300
+ align-items: center;
301
+ gap: 12px;
302
+ }
303
+
304
+ .agent-actions-header {
305
+ display: flex;
306
+ align-items: center;
307
+ gap: 8px;
308
+ }
309
+
245
310
  .card-header h2 {
246
311
  font-size: 1.3rem;
247
312
  font-weight: 600;
@@ -298,6 +363,11 @@ body {
298
363
  color: #2e7d32;
299
364
  }
300
365
 
366
+ .agent-status.recent {
367
+ background: #fff3e0;
368
+ color: #f57c00;
369
+ }
370
+
301
371
  .agent-status.offline {
302
372
  background: #ffebee;
303
373
  color: #c62828;
@@ -356,6 +426,24 @@ body {
356
426
  transform: translateY(-1px);
357
427
  }
358
428
 
429
+ /* Disabled/Offline Agent Styles */
430
+ .agent-item.agent-offline {
431
+ opacity: 0.7;
432
+ }
433
+
434
+ .btn-disabled {
435
+ background: #f5f5f5 !important;
436
+ color: #999 !important;
437
+ cursor: not-allowed !important;
438
+ border: 1px solid #e0e0e0 !important;
439
+ }
440
+
441
+ .btn-disabled:hover {
442
+ background: #f5f5f5 !important;
443
+ transform: none !important;
444
+ color: #999 !important;
445
+ }
446
+
359
447
  /* Quick Actions */
360
448
  .action-buttons {
361
449
  display: flex;
@@ -852,4 +940,55 @@ body {
852
940
 
853
941
  .api-error button {
854
942
  margin-top: 10px;
943
+ }
944
+
945
+ /* Connection Testing States */
946
+ .btn-connect.testing {
947
+ background: #ffc107 !important;
948
+ color: #333 !important;
949
+ cursor: wait !important;
950
+ }
951
+
952
+ /* Connection Error Notifications */
953
+ .connection-error-notification {
954
+ position: fixed;
955
+ top: 120px;
956
+ right: 20px;
957
+ background: #fff;
958
+ border-left: 4px solid #f44336;
959
+ border-radius: 8px;
960
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
961
+ padding: 16px 20px;
962
+ max-width: 350px;
963
+ z-index: 3000;
964
+ animation: slideInRight 0.3s ease-out;
965
+ display: flex;
966
+ align-items: flex-start;
967
+ gap: 12px;
968
+ }
969
+
970
+ .connection-error-notification .error-content {
971
+ flex: 1;
972
+ font-size: 0.9rem;
973
+ line-height: 1.4;
974
+ }
975
+
976
+ .connection-error-notification button {
977
+ background: none;
978
+ border: none;
979
+ font-size: 1.2rem;
980
+ cursor: pointer;
981
+ color: #999;
982
+ padding: 0;
983
+ width: 20px;
984
+ height: 20px;
985
+ display: flex;
986
+ align-items: center;
987
+ justify-content: center;
988
+ border-radius: 50%;
989
+ }
990
+
991
+ .connection-error-notification button:hover {
992
+ background: #f5f5f5;
993
+ color: #333;
855
994
  }
@@ -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
 
@@ -297,16 +299,51 @@ class ShellMirrorDashboard {
297
299
  }
298
300
 
299
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;
300
331
  const connectionStatus = document.getElementById('connection-status');
301
332
  if (connectionStatus) {
302
333
  connectionStatus.className = `connection-status ${status}`;
303
- const statusText = {
304
- connected: '๐ŸŸข Live',
305
- disconnected: '๐ŸŸก Reconnecting...',
306
- error: '๐Ÿ”ด Connection Error',
307
- failed: '๐Ÿ”ด Offline'
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 }
308
342
  };
309
- connectionStatus.textContent = statusText[status] || 'โ“ Unknown';
343
+
344
+ const config = statusConfig[status] || { text: 'โ“ Unknown', show: true };
345
+ connectionStatus.textContent = config.text;
346
+ connectionStatus.style.display = config.show ? 'inline-block' : 'none';
310
347
  }
311
348
  }
312
349
 
@@ -465,14 +502,11 @@ class ShellMirrorDashboard {
465
502
  }
466
503
 
467
504
  renderAuthenticatedDashboard() {
468
- // Update user section with refresh button and status
505
+ // Update user section - simplified without refresh button
469
506
  document.getElementById('user-section').innerHTML = `
470
507
  <div class="dashboard-controls">
471
- <span id="connection-status" class="connection-status">๐ŸŸก Connecting...</span>
508
+ <span id="connection-status" class="connection-status" style="display: none;"></span>
472
509
  <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
510
  </div>
477
511
  <div class="user-info">
478
512
  <span class="user-name">${this.user.name || this.user.email}</span>
@@ -503,30 +537,62 @@ class ShellMirrorDashboard {
503
537
  }
504
538
 
505
539
  renderActiveAgents() {
506
- // Filter for recently active agents (online or seen within last 5 minutes)
507
- const activeAgents = this.agents.filter(agent => {
508
- const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
509
- return agent.onlineStatus === 'online' || timeSinceLastSeen < 300; // 5 minutes
540
+ // Filter for agents that should be displayed (online, recent, or offline with sessions)
541
+ const displayAgents = this.agents.filter(agent => {
542
+ // Always show online agents
543
+ if (agent.status === 'online') return true;
544
+
545
+ // Show recent agents (last 2 minutes)
546
+ if (agent.status === 'recent') return true;
547
+
548
+ // Show offline agents only if they have active sessions
549
+ const sessions = this.agentSessions[agent.agentId] || [];
550
+ const activeSessions = sessions.filter(s => s.status === 'active');
551
+ return activeSessions.length > 0;
510
552
  });
553
+
554
+ // Separate truly active vs inactive agents
555
+ const activeAgents = displayAgents.filter(agent => agent.status === 'online' || agent.status === 'recent');
511
556
 
512
- const agentCount = activeAgents.length;
513
- const agentsHtml = activeAgents.map(agent => {
557
+ const agentCount = displayAgents.length;
558
+ const agentsHtml = displayAgents.map(agent => {
514
559
  const sessions = this.agentSessions[agent.agentId] || [];
515
560
  const sessionCount = sessions.length;
516
561
 
562
+ const isConnectable = agent.status === 'online' || agent.status === 'recent';
563
+ const statusIcon = {
564
+ 'online': '๐ŸŸข',
565
+ 'recent': '๐ŸŸก',
566
+ 'offline': '๐Ÿ”ด'
567
+ }[agent.status] || 'โ“';
568
+
569
+ const statusText = {
570
+ 'online': 'Live',
571
+ 'recent': 'Recent',
572
+ 'offline': 'Offline'
573
+ }[agent.status] || agent.status;
574
+
575
+ const lastSeenText = agent.timeSinceLastSeen !== undefined
576
+ ? this.formatPreciseLastSeen(agent.timeSinceLastSeen)
577
+ : this.formatLastSeen(agent.lastSeen);
578
+
517
579
  return `
518
- <div class="agent-item">
580
+ <div class="agent-item ${!isConnectable ? 'agent-offline' : ''}">
519
581
  <div class="agent-info">
520
582
  <div class="agent-name">${agent.machineName || agent.agentId}</div>
521
- <div class="agent-status ${agent.onlineStatus}">${agent.onlineStatus}</div>
522
- <div class="agent-last-seen">Last seen: ${this.formatLastSeen(agent.lastSeen)}</div>
583
+ <div class="agent-status ${agent.status}">
584
+ ${statusIcon} ${statusText}
585
+ </div>
586
+ <div class="agent-last-seen">Last seen: ${lastSeenText}</div>
523
587
  ${sessionCount > 0 ? `<div class="agent-sessions">${sessionCount} active session${sessionCount !== 1 ? 's' : ''}</div>` : ''}
524
588
  </div>
525
589
  <div class="agent-actions">
526
- <button class="btn-connect" onclick="dashboard.connectToAgent('${agent.agentId}')">
527
- ${sessionCount > 0 ? 'Resume Session' : 'New Session'}
590
+ <button class="btn-connect ${!isConnectable ? 'btn-disabled' : ''}"
591
+ onclick="dashboard.connectToAgent('${agent.agentId}')"
592
+ ${!isConnectable ? 'disabled' : ''}>
593
+ ${!isConnectable ? 'Offline' : sessionCount > 0 ? 'Resume Session' : 'New Session'}
528
594
  </button>
529
- ${sessionCount > 0 ? `<button class="btn-sessions" onclick="dashboard.showAgentSessions('${agent.agentId}')">
595
+ ${sessionCount > 0 && isConnectable ? `<button class="btn-sessions" onclick="dashboard.showAgentSessions('${agent.agentId}')">
530
596
  All Sessions
531
597
  </button>` : ''}
532
598
  </div>
@@ -534,11 +600,25 @@ class ShellMirrorDashboard {
534
600
  `;
535
601
  }).join('');
536
602
 
603
+ // Check if there are any offline agents to show cleanup option
604
+ const offlineAgents = this.agents.filter(agent => agent.status === 'offline');
605
+ const showCleanup = offlineAgents.length > 0;
606
+
537
607
  return `
538
608
  <div class="dashboard-card">
539
609
  <div class="card-header">
540
- <h2>๐Ÿ–ฅ๏ธ Active Agents</h2>
541
- <span class="agent-count">${agentCount} agent${agentCount !== 1 ? 's' : ''}</span>
610
+ <div class="card-title-section">
611
+ <h2>๐Ÿ–ฅ๏ธ Active Agents</h2>
612
+ <span class="agent-count">${agentCount} agent${agentCount !== 1 ? 's' : ''}</span>
613
+ </div>
614
+ <div class="agent-actions-header">
615
+ <button id="refresh-btn" class="refresh-btn-inline" onclick="dashboard.manualRefresh()" title="Refresh agents">
616
+ <span class="refresh-icon">๐Ÿ”„</span>
617
+ </button>
618
+ ${showCleanup ? `<button class="cleanup-btn-inline" onclick="dashboard.cleanupOfflineAgents()" title="Remove offline agents">
619
+ <span>๐Ÿงน</span>
620
+ </button>` : ''}
621
+ </div>
542
622
  </div>
543
623
  <div class="card-content">
544
624
  ${agentCount > 0 ? agentsHtml : '<p class="no-data">No active agents. <a href="#" onclick="dashboard.showAgentInstructions()">Set up an agent</a></p>'}
@@ -710,6 +790,16 @@ class ShellMirrorDashboard {
710
790
  return `${Math.floor(diff / 86400)} days ago`;
711
791
  }
712
792
 
793
+ formatPreciseLastSeen(timeSinceLastSeen) {
794
+ if (!timeSinceLastSeen && timeSinceLastSeen !== 0) return 'Unknown';
795
+
796
+ if (timeSinceLastSeen < 10) return 'Just now';
797
+ if (timeSinceLastSeen < 60) return `${Math.floor(timeSinceLastSeen)} seconds ago`;
798
+ if (timeSinceLastSeen < 3600) return `${Math.floor(timeSinceLastSeen / 60)} minutes ago`;
799
+ if (timeSinceLastSeen < 86400) return `${Math.floor(timeSinceLastSeen / 3600)} hours ago`;
800
+ return `${Math.floor(timeSinceLastSeen / 86400)} days ago`;
801
+ }
802
+
713
803
  formatDate(date) {
714
804
  return new Intl.DateTimeFormat('en-US', {
715
805
  month: 'short',
@@ -743,9 +833,109 @@ class ShellMirrorDashboard {
743
833
  }
744
834
  }
745
835
 
836
+ showAgentConnectionTest(agentId, status) {
837
+ // Find the agent item in the DOM and show loading state
838
+ const agentItems = document.querySelectorAll('.agent-item');
839
+ agentItems.forEach(item => {
840
+ const connectBtn = item.querySelector('.btn-connect');
841
+ if (connectBtn && connectBtn.onclick.toString().includes(agentId)) {
842
+ if (status === 'testing') {
843
+ connectBtn.textContent = 'Testing...';
844
+ connectBtn.disabled = true;
845
+ connectBtn.classList.add('testing');
846
+ } else if (status === 'done') {
847
+ connectBtn.disabled = false;
848
+ connectBtn.classList.remove('testing');
849
+ // Original text will be restored on next refresh
850
+ }
851
+ }
852
+ });
853
+ }
854
+
855
+ showConnectionError(agent, message) {
856
+ // Show a more user-friendly error message
857
+ const notification = document.createElement('div');
858
+ notification.className = 'connection-error-notification';
859
+ notification.innerHTML = `
860
+ <div class="error-content">
861
+ <strong>Connection Failed</strong><br>
862
+ Agent: ${agent.machineName || agent.agentId}<br>
863
+ ${message}
864
+ </div>
865
+ <button onclick="this.parentElement.remove()">ร—</button>
866
+ `;
867
+ document.body.appendChild(notification);
868
+
869
+ // Auto-remove after 10 seconds
870
+ setTimeout(() => {
871
+ if (document.body.contains(notification)) {
872
+ document.body.removeChild(notification);
873
+ }
874
+ }, 10000);
875
+ }
876
+
877
+ async testAgentConnectivity(agentId) {
878
+ try {
879
+ console.log('[DASHBOARD] ๐Ÿ” Testing connectivity for agent:', agentId);
880
+
881
+ // Try to ping the agent through the API
882
+ const response = await fetch(`/php-backend/api/ping-agent.php?agentId=${encodeURIComponent(agentId)}`, {
883
+ method: 'GET',
884
+ credentials: 'include',
885
+ timeout: 5000 // 5 second timeout
886
+ });
887
+
888
+ if (response.ok) {
889
+ const data = await response.json();
890
+ console.log('[DASHBOARD] ๐Ÿ” Ping response:', data);
891
+ return data.success && data.reachable;
892
+ }
893
+
894
+ console.log('[DASHBOARD] โš ๏ธ Ping request failed:', response.status);
895
+ return false;
896
+ } catch (error) {
897
+ console.log('[DASHBOARD] โš ๏ธ Agent connectivity test failed:', error);
898
+ return false;
899
+ }
900
+ }
901
+
746
902
  // Action handlers
747
903
  async connectToAgent(agentId) {
748
904
  console.log('[DASHBOARD] ๐Ÿ” DEBUG: connectToAgent called with agentId:', agentId);
905
+
906
+ // First, test if agent is actually reachable
907
+ const agent = this.agents.find(a => a.agentId === agentId);
908
+ if (!agent) {
909
+ alert('Agent not found. Please refresh and try again.');
910
+ return;
911
+ }
912
+
913
+ // Check agent status
914
+ if (agent.status === 'offline') {
915
+ alert(`Agent "${agent.machineName || agentId}" is offline. Please ensure the agent is running.`);
916
+ return;
917
+ }
918
+
919
+ // Test agent connectivity for recent agents with loading indicator
920
+ if (agent.status === 'recent') {
921
+ console.log('[DASHBOARD] ๐Ÿ” Testing agent connectivity...');
922
+
923
+ // Show loading state on the specific agent
924
+ this.showAgentConnectionTest(agentId, 'testing');
925
+
926
+ const isReachable = await this.testAgentConnectivity(agentId);
927
+
928
+ // Hide loading state
929
+ this.showAgentConnectionTest(agentId, 'done');
930
+
931
+ if (!isReachable) {
932
+ this.showConnectionError(agent, 'Connection test failed - agent may be offline');
933
+ // Refresh agent list to get updated status
934
+ await this.refreshDashboardData();
935
+ return;
936
+ }
937
+ }
938
+
749
939
  console.log('[DASHBOARD] ๐Ÿ” DEBUG: Current agentSessions:', this.agentSessions);
750
940
 
751
941
  // Check if there are existing sessions for this agent
@@ -821,16 +1011,15 @@ class ShellMirrorDashboard {
821
1011
  }
822
1012
 
823
1013
  startNewSession() {
824
- // Get first available agent for new session
825
- const activeAgents = this.agents.filter(agent => {
826
- const timeSinceLastSeen = Date.now() / 1000 - agent.lastSeen;
827
- return agent.onlineStatus === 'online' || timeSinceLastSeen < 300;
828
- });
1014
+ // Get first available agent for new session (only truly active ones)
1015
+ const activeAgents = this.agents.filter(agent =>
1016
+ agent.status === 'online' || agent.status === 'recent'
1017
+ );
829
1018
 
830
1019
  if (activeAgents.length > 0) {
831
1020
  this.connectToAgent(activeAgents[0].agentId);
832
1021
  } else {
833
- alert('No active agents available. Please ensure an agent is running on your Mac.');
1022
+ alert('No active agents available. Please ensure an agent is running on your Mac and try refreshing.');
834
1023
  }
835
1024
  }
836
1025
 
@@ -951,6 +1140,76 @@ class ShellMirrorDashboard {
951
1140
  }
952
1141
  }
953
1142
 
1143
+ async cleanupOfflineAgents() {
1144
+ try {
1145
+ console.log('[DASHBOARD] ๐Ÿงน Cleaning up offline agents...');
1146
+
1147
+ const response = await fetch('/php-backend/api/cleanup-agents.php', {
1148
+ method: 'POST',
1149
+ credentials: 'include'
1150
+ });
1151
+
1152
+ if (response.ok) {
1153
+ const data = await response.json();
1154
+ console.log('[DASHBOARD] โœ… Cleanup response:', data);
1155
+
1156
+ if (data.success) {
1157
+ // Show success message
1158
+ this.showCleanupResult(data.data.message);
1159
+
1160
+ // Refresh the agent list
1161
+ await this.refreshDashboardData();
1162
+ } else {
1163
+ this.showCleanupError('Cleanup failed: ' + (data.message || 'Unknown error'));
1164
+ }
1165
+ } else {
1166
+ this.showCleanupError('Cleanup request failed: ' + response.status);
1167
+ }
1168
+ } catch (error) {
1169
+ console.error('[DASHBOARD] โŒ Cleanup failed:', error);
1170
+ this.showCleanupError('Cleanup failed: ' + error.message);
1171
+ }
1172
+ }
1173
+
1174
+ showCleanupResult(message) {
1175
+ const notification = document.createElement('div');
1176
+ notification.className = 'cleanup-result-notification';
1177
+ notification.innerHTML = `
1178
+ <div class="result-content">
1179
+ <strong>โœ… Cleanup Complete</strong><br>
1180
+ ${message}
1181
+ </div>
1182
+ <button onclick="this.parentElement.remove()">ร—</button>
1183
+ `;
1184
+ document.body.appendChild(notification);
1185
+
1186
+ // Auto-remove after 5 seconds
1187
+ setTimeout(() => {
1188
+ if (document.body.contains(notification)) {
1189
+ document.body.removeChild(notification);
1190
+ }
1191
+ }, 5000);
1192
+ }
1193
+
1194
+ showCleanupError(message) {
1195
+ const notification = document.createElement('div');
1196
+ notification.className = 'connection-error-notification';
1197
+ notification.innerHTML = `
1198
+ <div class="error-content">
1199
+ <strong>Cleanup Failed</strong><br>
1200
+ ${message}
1201
+ </div>
1202
+ <button onclick="this.parentElement.remove()">ร—</button>
1203
+ `;
1204
+ document.body.appendChild(notification);
1205
+
1206
+ // Auto-remove after 8 seconds
1207
+ setTimeout(() => {
1208
+ if (document.body.contains(notification)) {
1209
+ document.body.removeChild(notification);
1210
+ }
1211
+ }, 8000);
1212
+ }
954
1213
 
955
1214
  showAgentInstructions() {
956
1215
  // TODO: Show modal with agent setup instructions