shell-mirror 1.5.33 → 1.5.35

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/README.md CHANGED
@@ -146,12 +146,14 @@ Before you can run the application, you need to get a **Client ID** and **Client
146
146
  - Open your web browser and go to `https://shellmirror.app`
147
147
  - You will be prompted to log in with your Google account
148
148
  - Once authenticated, you will see your shell mirrored in the browser
149
+ - **macOS users:** You may see a firewall dialog asking if Node.js can accept incoming connections - click "Allow" for full terminal functionality
149
150
 
150
151
  **For Local Development:**
151
152
  - Open your web browser and go to `http://localhost:3000`
152
153
  - Or access from another device on the same network: `http://<your-macs-ip-address>:3000`
153
154
  - You will be prompted to log in with your Google account
154
155
  - Once authenticated, you will see your shell mirrored in the browser
156
+ - **macOS users:** You may see a firewall dialog asking if Node.js can accept incoming connections - click "Allow" for full terminal functionality
155
157
 
156
158
  ### 6. Deployment Notes
157
159
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shell-mirror",
3
- "version": "1.5.33",
3
+ "version": "1.5.35",
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": {
@@ -0,0 +1,558 @@
1
+ /* Shell Mirror Dashboard Styles */
2
+
3
+ * {
4
+ margin: 0;
5
+ padding: 0;
6
+ box-sizing: border-box;
7
+ }
8
+
9
+ body {
10
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
11
+ line-height: 1.6;
12
+ color: #333;
13
+ background: #f5f7fa;
14
+ min-height: 100vh;
15
+ }
16
+
17
+ /* Header */
18
+ .dashboard-header {
19
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
20
+ color: white;
21
+ padding: 20px 0;
22
+ box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
23
+ }
24
+
25
+ .header-content {
26
+ max-width: 1200px;
27
+ margin: 0 auto;
28
+ padding: 0 20px;
29
+ display: flex;
30
+ justify-content: space-between;
31
+ align-items: center;
32
+ }
33
+
34
+ .logo h1 {
35
+ font-size: 1.8rem;
36
+ font-weight: 700;
37
+ margin-bottom: 2px;
38
+ }
39
+
40
+ .logo .subtitle {
41
+ font-size: 0.9rem;
42
+ opacity: 0.8;
43
+ }
44
+
45
+ .user-section {
46
+ display: flex;
47
+ align-items: center;
48
+ gap: 15px;
49
+ }
50
+
51
+ .user-info {
52
+ display: flex;
53
+ align-items: center;
54
+ gap: 10px;
55
+ position: relative;
56
+ }
57
+
58
+ .user-name {
59
+ font-weight: 500;
60
+ }
61
+
62
+ .user-dropdown {
63
+ position: relative;
64
+ }
65
+
66
+ .dropdown-btn {
67
+ background: rgba(255, 255, 255, 0.2);
68
+ border: 1px solid rgba(255, 255, 255, 0.3);
69
+ border-radius: 8px;
70
+ color: white;
71
+ padding: 8px 12px;
72
+ cursor: pointer;
73
+ transition: all 0.2s ease;
74
+ }
75
+
76
+ .dropdown-btn:hover {
77
+ background: rgba(255, 255, 255, 0.3);
78
+ }
79
+
80
+ .dropdown-content {
81
+ display: none;
82
+ position: absolute;
83
+ right: 0;
84
+ top: 100%;
85
+ background: white;
86
+ min-width: 150px;
87
+ box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
88
+ border-radius: 8px;
89
+ overflow: hidden;
90
+ z-index: 1000;
91
+ }
92
+
93
+ .dropdown-content a {
94
+ color: #333;
95
+ padding: 12px 16px;
96
+ text-decoration: none;
97
+ display: block;
98
+ transition: background 0.2s ease;
99
+ }
100
+
101
+ .dropdown-content a:hover {
102
+ background: #f8f9fa;
103
+ }
104
+
105
+ .user-dropdown:hover .dropdown-content {
106
+ display: block;
107
+ }
108
+
109
+ /* Buttons */
110
+ .btn-primary {
111
+ background: #4285F4;
112
+ color: white;
113
+ padding: 12px 24px;
114
+ border: none;
115
+ border-radius: 8px;
116
+ font-weight: 600;
117
+ cursor: pointer;
118
+ transition: all 0.2s ease;
119
+ display: inline-flex;
120
+ align-items: center;
121
+ text-decoration: none;
122
+ }
123
+
124
+ .btn-primary:hover {
125
+ background: #3367d6;
126
+ transform: translateY(-1px);
127
+ }
128
+
129
+ .btn-primary.small {
130
+ padding: 8px 16px;
131
+ font-size: 0.9rem;
132
+ }
133
+
134
+ /* Main Dashboard */
135
+ .dashboard-main {
136
+ max-width: 1200px;
137
+ margin: 0 auto;
138
+ padding: 40px 20px;
139
+ }
140
+
141
+ .dashboard-grid {
142
+ display: grid;
143
+ grid-template-columns: 1fr 300px;
144
+ gap: 30px;
145
+ margin-bottom: 30px;
146
+ }
147
+
148
+ .dashboard-card {
149
+ background: white;
150
+ border-radius: 12px;
151
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.05);
152
+ padding: 24px;
153
+ transition: all 0.2s ease;
154
+ }
155
+
156
+ .dashboard-card:hover {
157
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
158
+ }
159
+
160
+ .dashboard-card.full-width {
161
+ grid-column: 1 / -1;
162
+ }
163
+
164
+ .card-header {
165
+ display: flex;
166
+ justify-content: space-between;
167
+ align-items: center;
168
+ margin-bottom: 20px;
169
+ }
170
+
171
+ .card-header h2 {
172
+ font-size: 1.3rem;
173
+ font-weight: 600;
174
+ color: #333;
175
+ }
176
+
177
+ .agent-count {
178
+ background: #e3f2fd;
179
+ color: #1976d2;
180
+ padding: 4px 8px;
181
+ border-radius: 12px;
182
+ font-size: 0.8rem;
183
+ font-weight: 500;
184
+ }
185
+
186
+ .card-content {
187
+ color: #666;
188
+ }
189
+
190
+ /* Agent Items */
191
+ .agent-item {
192
+ display: flex;
193
+ justify-content: space-between;
194
+ align-items: center;
195
+ padding: 16px 0;
196
+ border-bottom: 1px solid #f0f0f0;
197
+ }
198
+
199
+ .agent-item:last-child {
200
+ border-bottom: none;
201
+ }
202
+
203
+ .agent-info {
204
+ flex: 1;
205
+ }
206
+
207
+ .agent-name {
208
+ font-weight: 600;
209
+ color: #333;
210
+ margin-bottom: 4px;
211
+ }
212
+
213
+ .agent-status {
214
+ display: inline-block;
215
+ padding: 2px 8px;
216
+ border-radius: 10px;
217
+ font-size: 0.75rem;
218
+ font-weight: 500;
219
+ text-transform: uppercase;
220
+ }
221
+
222
+ .agent-status.online {
223
+ background: #e8f5e8;
224
+ color: #2e7d32;
225
+ }
226
+
227
+ .agent-status.offline {
228
+ background: #ffebee;
229
+ color: #c62828;
230
+ }
231
+
232
+ .agent-last-seen {
233
+ font-size: 0.8rem;
234
+ color: #999;
235
+ margin-top: 2px;
236
+ }
237
+
238
+ .btn-connect {
239
+ background: #4caf50;
240
+ color: white;
241
+ border: none;
242
+ padding: 8px 16px;
243
+ border-radius: 6px;
244
+ font-weight: 500;
245
+ cursor: pointer;
246
+ transition: all 0.2s ease;
247
+ }
248
+
249
+ .btn-connect:hover {
250
+ background: #45a049;
251
+ transform: translateY(-1px);
252
+ }
253
+
254
+ /* Quick Actions */
255
+ .action-buttons {
256
+ display: flex;
257
+ flex-direction: column;
258
+ gap: 12px;
259
+ }
260
+
261
+ .action-btn {
262
+ display: flex;
263
+ align-items: center;
264
+ gap: 12px;
265
+ padding: 16px;
266
+ background: #f8f9fa;
267
+ border: 1px solid #e9ecef;
268
+ border-radius: 8px;
269
+ cursor: pointer;
270
+ transition: all 0.2s ease;
271
+ text-align: left;
272
+ }
273
+
274
+ .action-btn:hover {
275
+ background: #e9ecef;
276
+ border-color: #dee2e6;
277
+ }
278
+
279
+ .action-icon {
280
+ font-size: 1.2rem;
281
+ }
282
+
283
+ .action-text {
284
+ font-weight: 500;
285
+ color: #333;
286
+ }
287
+
288
+ /* Session Items */
289
+ .session-item {
290
+ display: flex;
291
+ justify-content: space-between;
292
+ align-items: center;
293
+ padding: 16px 0;
294
+ border-bottom: 1px solid #f0f0f0;
295
+ }
296
+
297
+ .session-item:last-child {
298
+ border-bottom: none;
299
+ }
300
+
301
+ .session-info {
302
+ flex: 1;
303
+ }
304
+
305
+ .session-agent {
306
+ font-weight: 500;
307
+ color: #333;
308
+ font-family: monospace;
309
+ font-size: 0.9rem;
310
+ }
311
+
312
+ .session-time {
313
+ font-size: 0.8rem;
314
+ color: #999;
315
+ margin-top: 2px;
316
+ }
317
+
318
+ .session-details {
319
+ display: flex;
320
+ flex-direction: column;
321
+ align-items: flex-end;
322
+ gap: 4px;
323
+ }
324
+
325
+ .session-duration {
326
+ font-size: 0.8rem;
327
+ color: #666;
328
+ }
329
+
330
+ .session-status {
331
+ padding: 2px 8px;
332
+ border-radius: 10px;
333
+ font-size: 0.7rem;
334
+ font-weight: 500;
335
+ text-transform: uppercase;
336
+ }
337
+
338
+ .session-status.completed {
339
+ background: #e8f5e8;
340
+ color: #2e7d32;
341
+ }
342
+
343
+ .session-status.active {
344
+ background: #fff3e0;
345
+ color: #f57c00;
346
+ }
347
+
348
+ /* Empty States */
349
+ .no-data {
350
+ text-align: center;
351
+ color: #999;
352
+ font-style: italic;
353
+ }
354
+
355
+ .no-data a {
356
+ color: #4285F4;
357
+ text-decoration: none;
358
+ }
359
+
360
+ .no-data a:hover {
361
+ text-decoration: underline;
362
+ }
363
+
364
+ /* Login Overlay */
365
+ .login-overlay {
366
+ position: fixed;
367
+ top: 0;
368
+ left: 0;
369
+ width: 100%;
370
+ height: 100%;
371
+ background: rgba(0, 0, 0, 0.8);
372
+ display: flex;
373
+ align-items: center;
374
+ justify-content: center;
375
+ z-index: 1000;
376
+ }
377
+
378
+ .login-modal {
379
+ background: white;
380
+ border-radius: 16px;
381
+ padding: 40px;
382
+ max-width: 500px;
383
+ width: 90%;
384
+ text-align: center;
385
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
386
+ }
387
+
388
+ .login-content h2 {
389
+ margin-bottom: 12px;
390
+ color: #333;
391
+ }
392
+
393
+ .login-content p {
394
+ color: #666;
395
+ margin-bottom: 30px;
396
+ }
397
+
398
+ .dashboard-preview {
399
+ display: grid;
400
+ grid-template-columns: repeat(auto-fit, minmax(140px, 1fr));
401
+ gap: 20px;
402
+ margin: 30px 0;
403
+ }
404
+
405
+ .preview-section {
406
+ padding: 20px;
407
+ background: #f8f9fa;
408
+ border-radius: 8px;
409
+ text-align: center;
410
+ }
411
+
412
+ .preview-section h3 {
413
+ font-size: 1rem;
414
+ margin-bottom: 8px;
415
+ color: #333;
416
+ }
417
+
418
+ .preview-section p {
419
+ font-size: 0.8rem;
420
+ color: #666;
421
+ margin: 0;
422
+ }
423
+
424
+ .login-note {
425
+ margin-top: 20px;
426
+ font-size: 0.85rem;
427
+ color: #999;
428
+ }
429
+
430
+ /* Dashboard Skeleton (for unauthenticated preview) */
431
+ .dashboard-skeleton .dashboard-card.blurred {
432
+ filter: blur(2px);
433
+ opacity: 0.6;
434
+ pointer-events: none;
435
+ }
436
+
437
+ .skeleton-content {
438
+ display: flex;
439
+ flex-direction: column;
440
+ gap: 12px;
441
+ }
442
+
443
+ .skeleton-agent,
444
+ .skeleton-action,
445
+ .skeleton-session {
446
+ height: 60px;
447
+ background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
448
+ background-size: 200% 100%;
449
+ animation: loading 1.5s infinite;
450
+ border-radius: 8px;
451
+ }
452
+
453
+ .skeleton-action {
454
+ height: 50px;
455
+ }
456
+
457
+ .skeleton-session {
458
+ height: 40px;
459
+ }
460
+
461
+ @keyframes loading {
462
+ 0% {
463
+ background-position: 200% 0;
464
+ }
465
+ 100% {
466
+ background-position: -200% 0;
467
+ }
468
+ }
469
+
470
+ /* Loading Overlay */
471
+ .loading-overlay {
472
+ position: fixed;
473
+ top: 0;
474
+ left: 0;
475
+ width: 100%;
476
+ height: 100%;
477
+ background: rgba(255, 255, 255, 0.9);
478
+ display: flex;
479
+ flex-direction: column;
480
+ align-items: center;
481
+ justify-content: center;
482
+ z-index: 2000;
483
+ }
484
+
485
+ .loading-spinner {
486
+ width: 40px;
487
+ height: 40px;
488
+ border: 4px solid #f3f3f3;
489
+ border-top: 4px solid #4285F4;
490
+ border-radius: 50%;
491
+ animation: spin 1s linear infinite;
492
+ margin-bottom: 20px;
493
+ }
494
+
495
+ @keyframes spin {
496
+ 0% { transform: rotate(0deg); }
497
+ 100% { transform: rotate(360deg); }
498
+ }
499
+
500
+ /* Error State */
501
+ .error-state {
502
+ text-align: center;
503
+ padding: 60px 20px;
504
+ color: #666;
505
+ }
506
+
507
+ .error-state h2 {
508
+ margin-bottom: 16px;
509
+ color: #333;
510
+ }
511
+
512
+ .error-state p {
513
+ margin-bottom: 24px;
514
+ }
515
+
516
+ /* Responsive Design */
517
+ @media (max-width: 768px) {
518
+ .dashboard-grid {
519
+ grid-template-columns: 1fr;
520
+ gap: 20px;
521
+ }
522
+
523
+ .header-content {
524
+ flex-direction: column;
525
+ gap: 15px;
526
+ text-align: center;
527
+ }
528
+
529
+ .dashboard-main {
530
+ padding: 20px 15px;
531
+ }
532
+
533
+ .dashboard-card {
534
+ padding: 20px;
535
+ }
536
+
537
+ .login-modal {
538
+ padding: 30px 20px;
539
+ }
540
+
541
+ .dashboard-preview {
542
+ grid-template-columns: 1fr;
543
+ gap: 15px;
544
+ }
545
+
546
+ .agent-item,
547
+ .session-item {
548
+ flex-direction: column;
549
+ align-items: flex-start;
550
+ gap: 12px;
551
+ }
552
+
553
+ .session-details {
554
+ align-items: flex-start;
555
+ flex-direction: row;
556
+ gap: 12px;
557
+ }
558
+ }
@@ -0,0 +1,76 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Shell Mirror Dashboard</title>
7
+ <link rel="stylesheet" href="dashboard.css">
8
+ </head>
9
+ <body>
10
+ <!-- Header -->
11
+ <header class="dashboard-header">
12
+ <div class="header-content">
13
+ <div class="logo">
14
+ <h1>Shell Mirror</h1>
15
+ <span class="subtitle">Dashboard</span>
16
+ </div>
17
+ <div class="user-section" id="user-section">
18
+ <!-- Dynamic content based on auth status -->
19
+ </div>
20
+ </div>
21
+ </header>
22
+
23
+ <!-- Main Dashboard Content -->
24
+ <main class="dashboard-main" id="dashboard-main">
25
+ <!-- Will be populated by JavaScript based on auth status -->
26
+ </main>
27
+
28
+ <!-- Login Overlay (for unauthenticated users) -->
29
+ <div class="login-overlay" id="login-overlay" style="display: none;">
30
+ <div class="login-modal">
31
+ <div class="login-content">
32
+ <h2>Welcome to Shell Mirror</h2>
33
+ <p>Sign in to access your dashboard and manage your terminal sessions</p>
34
+
35
+ <!-- Dashboard Preview -->
36
+ <div class="dashboard-preview">
37
+ <div class="preview-section">
38
+ <h3>🖥️ Active Agents</h3>
39
+ <p>See and connect to your running Mac agents</p>
40
+ </div>
41
+ <div class="preview-section">
42
+ <h3>📊 Session History</h3>
43
+ <p>Track your past terminal sessions</p>
44
+ </div>
45
+ <div class="preview-section">
46
+ <h3>⚙️ Quick Actions</h3>
47
+ <p>Manage settings and download agents</p>
48
+ </div>
49
+ </div>
50
+
51
+ <button class="btn-primary" onclick="handleLogin()">
52
+ <svg width="20" height="20" viewBox="0 0 24 24" style="margin-right: 8px;">
53
+ <path fill="white" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92c-.26 1.37-1.04 2.53-2.21 3.31v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.09z"/>
54
+ <path fill="white" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
55
+ <path fill="white" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
56
+ <path fill="white" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
57
+ </svg>
58
+ Sign in with Google
59
+ </button>
60
+
61
+ <p class="login-note">
62
+ Your credentials are secure and encrypted. We use Google OAuth 2.0 for authentication.
63
+ </p>
64
+ </div>
65
+ </div>
66
+ </div>
67
+
68
+ <!-- Loading indicator -->
69
+ <div class="loading-overlay" id="loading-overlay">
70
+ <div class="loading-spinner"></div>
71
+ <p>Loading dashboard...</p>
72
+ </div>
73
+
74
+ <script src="dashboard.js"></script>
75
+ </body>
76
+ </html>
@@ -0,0 +1,363 @@
1
+ // Dashboard functionality for Shell Mirror
2
+ class ShellMirrorDashboard {
3
+ constructor() {
4
+ this.isAuthenticated = false;
5
+ this.user = null;
6
+ this.agents = [];
7
+ this.sessions = [];
8
+ this.init();
9
+ }
10
+
11
+ async init() {
12
+ this.showLoading();
13
+
14
+ try {
15
+ const authStatus = await this.checkAuthStatus();
16
+
17
+ if (authStatus.isAuthenticated) {
18
+ this.isAuthenticated = true;
19
+ this.user = authStatus.user;
20
+ await this.loadDashboardData();
21
+ this.renderAuthenticatedDashboard();
22
+ } else {
23
+ this.renderUnauthenticatedDashboard();
24
+ }
25
+ } catch (error) {
26
+ console.error('Dashboard initialization failed:', error);
27
+ this.renderErrorState();
28
+ } finally {
29
+ this.hideLoading();
30
+ }
31
+ }
32
+
33
+ showLoading() {
34
+ document.getElementById('loading-overlay').style.display = 'flex';
35
+ }
36
+
37
+ hideLoading() {
38
+ document.getElementById('loading-overlay').style.display = 'none';
39
+ }
40
+
41
+ async checkAuthStatus() {
42
+ try {
43
+ const response = await fetch('/php-backend/api/auth-status.php');
44
+ const data = await response.json();
45
+
46
+ if (data.success && data.data && data.data.authenticated) {
47
+ return { isAuthenticated: true, user: data.data };
48
+ }
49
+ } catch (error) {
50
+ console.log('Auth check failed:', error);
51
+ }
52
+ return { isAuthenticated: false, user: null };
53
+ }
54
+
55
+ async loadDashboardData() {
56
+ try {
57
+ // Load active agents
58
+ const agentsResponse = await fetch('/php-backend/api/agents-list.php');
59
+ const agentsData = await agentsResponse.json();
60
+
61
+ if (agentsData.success && agentsData.data && agentsData.data.agents) {
62
+ this.agents = agentsData.data.agents;
63
+ }
64
+
65
+ // TODO: Load session history when API is available
66
+ this.sessions = [
67
+ {
68
+ id: 1,
69
+ agentId: 'local-MacBookPro-1755126622411',
70
+ startTime: new Date('2025-08-13T22:10:00Z'),
71
+ duration: '15 minutes',
72
+ status: 'completed'
73
+ },
74
+ {
75
+ id: 2,
76
+ agentId: 'local-MacBookPro-1755123565749',
77
+ startTime: new Date('2025-08-13T21:30:00Z'),
78
+ duration: '45 minutes',
79
+ status: 'completed'
80
+ }
81
+ ];
82
+
83
+ } catch (error) {
84
+ console.error('Failed to load dashboard data:', error);
85
+ }
86
+ }
87
+
88
+ renderUnauthenticatedDashboard() {
89
+ // Update user section
90
+ document.getElementById('user-section').innerHTML = `
91
+ <button class="btn-primary small" onclick="handleLogin()">Sign In</button>
92
+ `;
93
+
94
+ // Show actual dashboard content but blurred
95
+ document.getElementById('dashboard-main').innerHTML = `
96
+ <div class="dashboard-skeleton">
97
+ <div class="dashboard-grid">
98
+ <div class="dashboard-card blurred">
99
+ ${this.renderActiveAgentsPreview()}
100
+ </div>
101
+
102
+ <div class="dashboard-card blurred">
103
+ ${this.renderQuickActions()}
104
+ </div>
105
+
106
+ <div class="dashboard-card blurred full-width">
107
+ ${this.renderRecentSessionsPreview()}
108
+ </div>
109
+ </div>
110
+ </div>
111
+ `;
112
+
113
+ // Show login overlay
114
+ document.getElementById('login-overlay').style.display = 'flex';
115
+ }
116
+
117
+ renderAuthenticatedDashboard() {
118
+ // Update user section
119
+ document.getElementById('user-section').innerHTML = `
120
+ <div class="user-info">
121
+ <span class="user-name">${this.user.name || this.user.email}</span>
122
+ <div class="user-dropdown">
123
+ <button class="dropdown-btn">⚙️</button>
124
+ <div class="dropdown-content">
125
+ <a href="#" onclick="dashboard.logout()">Logout</a>
126
+ <a href="/">Home</a>
127
+ </div>
128
+ </div>
129
+ </div>
130
+ `;
131
+
132
+ // Render main dashboard content
133
+ document.getElementById('dashboard-main').innerHTML = `
134
+ <div class="dashboard-grid">
135
+ ${this.renderActiveAgents()}
136
+ ${this.renderQuickActions()}
137
+ ${this.renderRecentSessions()}
138
+ </div>
139
+ `;
140
+
141
+ // Hide login overlay
142
+ document.getElementById('login-overlay').style.display = 'none';
143
+ }
144
+
145
+ renderActiveAgents() {
146
+ const agentCount = this.agents.length;
147
+ const agentsHtml = this.agents.map(agent => `
148
+ <div class="agent-item">
149
+ <div class="agent-info">
150
+ <div class="agent-name">${agent.machineName || agent.agentId}</div>
151
+ <div class="agent-status ${agent.onlineStatus}">${agent.onlineStatus}</div>
152
+ <div class="agent-last-seen">Last seen: ${this.formatLastSeen(agent.lastSeen)}</div>
153
+ </div>
154
+ <button class="btn-connect" onclick="dashboard.connectToAgent('${agent.agentId}')">
155
+ Connect
156
+ </button>
157
+ </div>
158
+ `).join('');
159
+
160
+ return `
161
+ <div class="dashboard-card">
162
+ <div class="card-header">
163
+ <h2>🖥️ Active Agents</h2>
164
+ <span class="agent-count">${agentCount} agent${agentCount !== 1 ? 's' : ''}</span>
165
+ </div>
166
+ <div class="card-content">
167
+ ${agentCount > 0 ? agentsHtml : '<p class="no-data">No active agents. <a href="#" onclick="dashboard.showAgentInstructions()">Set up an agent</a></p>'}
168
+ </div>
169
+ </div>
170
+ `;
171
+ }
172
+
173
+ renderQuickActions() {
174
+ return `
175
+ <div class="dashboard-card">
176
+ <h2>⚡ Quick Actions</h2>
177
+ <div class="card-content">
178
+ <div class="action-buttons">
179
+ <button class="action-btn" onclick="dashboard.startNewSession()">
180
+ <span class="action-icon">🚀</span>
181
+ <span class="action-text">New Terminal Session</span>
182
+ </button>
183
+ <button class="action-btn" onclick="dashboard.showAgentInstructions()">
184
+ <span class="action-icon">📥</span>
185
+ <span class="action-text">Download Agent</span>
186
+ </button>
187
+ <button class="action-btn" onclick="dashboard.showSettings()">
188
+ <span class="action-icon">⚙️</span>
189
+ <span class="action-text">Settings</span>
190
+ </button>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ `;
195
+ }
196
+
197
+ renderRecentSessions() {
198
+ const sessionsHtml = this.sessions.map(session => `
199
+ <div class="session-item">
200
+ <div class="session-info">
201
+ <div class="session-agent">${session.agentId}</div>
202
+ <div class="session-time">${this.formatDate(session.startTime)}</div>
203
+ </div>
204
+ <div class="session-details">
205
+ <span class="session-duration">${session.duration}</span>
206
+ <span class="session-status ${session.status}">${session.status}</span>
207
+ </div>
208
+ </div>
209
+ `).join('');
210
+
211
+ return `
212
+ <div class="dashboard-card full-width">
213
+ <h2>📊 Recent Sessions</h2>
214
+ <div class="card-content">
215
+ ${this.sessions.length > 0 ? sessionsHtml : '<p class="no-data">No recent sessions</p>'}
216
+ </div>
217
+ </div>
218
+ `;
219
+ }
220
+
221
+ renderActiveAgentsPreview() {
222
+ // Show sample agent data for preview
223
+ const sampleAgents = [
224
+ { machineName: 'MacBook Pro', onlineStatus: 'online', lastSeen: Date.now() / 1000 - 60 },
225
+ { machineName: 'Mac Studio', onlineStatus: 'offline', lastSeen: Date.now() / 1000 - 3600 }
226
+ ];
227
+
228
+ const agentsHtml = sampleAgents.map(agent => `
229
+ <div class="agent-item">
230
+ <div class="agent-info">
231
+ <div class="agent-name">${agent.machineName}</div>
232
+ <div class="agent-status ${agent.onlineStatus}">${agent.onlineStatus}</div>
233
+ <div class="agent-last-seen">Last seen: ${this.formatLastSeen(agent.lastSeen)}</div>
234
+ </div>
235
+ <button class="btn-connect" disabled>
236
+ Connect
237
+ </button>
238
+ </div>
239
+ `).join('');
240
+
241
+ return `
242
+ <div class="card-header">
243
+ <h2>🖥️ Active Agents</h2>
244
+ <span class="agent-count">${sampleAgents.length} agents</span>
245
+ </div>
246
+ <div class="card-content">
247
+ ${agentsHtml}
248
+ </div>
249
+ `;
250
+ }
251
+
252
+ renderRecentSessionsPreview() {
253
+ // Show sample session data for preview
254
+ const sampleSessions = [
255
+ {
256
+ agentId: 'MacBook-Pro-xyz',
257
+ startTime: new Date(Date.now() - 86400000), // 1 day ago
258
+ duration: '45 minutes',
259
+ status: 'completed'
260
+ },
261
+ {
262
+ agentId: 'Mac-Studio-abc',
263
+ startTime: new Date(Date.now() - 3600000), // 1 hour ago
264
+ duration: '2 hours',
265
+ status: 'completed'
266
+ }
267
+ ];
268
+
269
+ const sessionsHtml = sampleSessions.map(session => `
270
+ <div class="session-item">
271
+ <div class="session-info">
272
+ <div class="session-agent">${session.agentId}</div>
273
+ <div class="session-time">${this.formatDate(session.startTime)}</div>
274
+ </div>
275
+ <div class="session-details">
276
+ <span class="session-duration">${session.duration}</span>
277
+ <span class="session-status ${session.status}">${session.status}</span>
278
+ </div>
279
+ </div>
280
+ `).join('');
281
+
282
+ return `
283
+ <h2>📊 Recent Sessions</h2>
284
+ <div class="card-content">
285
+ ${sessionsHtml}
286
+ </div>
287
+ `;
288
+ }
289
+
290
+ renderErrorState() {
291
+ document.getElementById('dashboard-main').innerHTML = `
292
+ <div class="error-state">
293
+ <h2>⚠️ Something went wrong</h2>
294
+ <p>Unable to load dashboard. Please try refreshing the page.</p>
295
+ <button class="btn-primary" onclick="location.reload()">Refresh</button>
296
+ </div>
297
+ `;
298
+ }
299
+
300
+ // Utility methods
301
+ formatLastSeen(lastSeen) {
302
+ if (!lastSeen) return 'Unknown';
303
+
304
+ const now = Date.now() / 1000;
305
+ const diff = now - lastSeen;
306
+
307
+ if (diff < 60) return 'Just now';
308
+ if (diff < 3600) return `${Math.floor(diff / 60)} minutes ago`;
309
+ if (diff < 86400) return `${Math.floor(diff / 3600)} hours ago`;
310
+ return `${Math.floor(diff / 86400)} days ago`;
311
+ }
312
+
313
+ formatDate(date) {
314
+ return new Intl.DateTimeFormat('en-US', {
315
+ month: 'short',
316
+ day: 'numeric',
317
+ hour: '2-digit',
318
+ minute: '2-digit'
319
+ }).format(date);
320
+ }
321
+
322
+ // Action handlers
323
+ async connectToAgent(agentId) {
324
+ window.location.href = `/app/terminal.html?agent=${agentId}`;
325
+ }
326
+
327
+ startNewSession() {
328
+ window.location.href = '/app/terminal.html';
329
+ }
330
+
331
+ showAgentInstructions() {
332
+ // TODO: Show modal with agent setup instructions
333
+ alert('Agent setup instructions coming soon!');
334
+ }
335
+
336
+ showSettings() {
337
+ // TODO: Implement settings modal
338
+ alert('Settings coming soon!');
339
+ }
340
+
341
+ async logout() {
342
+ try {
343
+ await fetch('/php-backend/api/auth-logout.php', { method: 'POST' });
344
+ window.location.href = '/';
345
+ } catch (error) {
346
+ console.error('Logout failed:', error);
347
+ // Fallback - redirect anyway
348
+ window.location.href = '/';
349
+ }
350
+ }
351
+ }
352
+
353
+ // Global functions
354
+ function handleLogin() {
355
+ const returnUrl = encodeURIComponent(window.location.pathname);
356
+ window.location.href = `/php-backend/api/auth-login.php?return=${returnUrl}`;
357
+ }
358
+
359
+ // Initialize dashboard
360
+ let dashboard;
361
+ document.addEventListener('DOMContentLoaded', () => {
362
+ dashboard = new ShellMirrorDashboard();
363
+ });
@@ -30,22 +30,43 @@
30
30
  #connect-container { padding: 2em; text-align: center; }
31
31
  #agent-id-input { font-size: 1.2em; padding: 8px; width: 400px; margin-bottom: 1em; }
32
32
  #connect-btn { font-size: 1.2em; padding: 10px 20px; }
33
+
34
+ /* Back to Dashboard Button */
35
+ .back-to-dashboard {
36
+ position: fixed;
37
+ top: 20px;
38
+ left: 20px;
39
+ background: rgba(66, 133, 244, 0.9);
40
+ color: white;
41
+ border: none;
42
+ padding: 12px 20px;
43
+ border-radius: 8px;
44
+ font-weight: 500;
45
+ cursor: pointer;
46
+ z-index: 1000;
47
+ transition: all 0.2s ease;
48
+ display: flex;
49
+ align-items: center;
50
+ gap: 8px;
51
+ text-decoration: none;
52
+ }
53
+
54
+ .back-to-dashboard:hover {
55
+ background: rgba(51, 103, 214, 0.9);
56
+ transform: translateY(-1px);
57
+ }
33
58
  </style>
34
59
  </head>
35
60
  <body>
61
+ <!-- Back to Dashboard Button -->
62
+ <a href="/app/dashboard.html" class="back-to-dashboard">
63
+ <span>←</span>
64
+ <span>Dashboard</span>
65
+ </a>
66
+
36
67
  <div id="connect-container">
37
68
  <h2>Terminal Mirror</h2>
38
- <div id="agent-discovery">
39
- <p>Discovering available Mac agents...</p>
40
- <div id="agent-list"></div>
41
- </div>
42
- <div id="manual-connect" style="display: none; margin-top: 20px;">
43
- <p>Or manually enter Agent ID:</p>
44
- <input type="text" id="agent-id-input" placeholder="e.g., agent-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx">
45
- <br>
46
- <button id="connect-btn">Connect</button>
47
- </div>
48
- <button id="show-manual" style="margin-top: 10px;">Manual Connect</button>
69
+ <p>Connecting to terminal...</p>
49
70
  </div>
50
71
  <div id="terminal-container">
51
72
  <div id="terminal"></div>
@@ -41,12 +41,6 @@ term.loadAddon(fitAddon);
41
41
 
42
42
  const connectContainer = document.getElementById('connect-container');
43
43
  const terminalContainer = document.getElementById('terminal-container');
44
- const agentIdInput = document.getElementById('agent-id-input');
45
- const connectBtn = document.getElementById('connect-btn');
46
- const showManualBtn = document.getElementById('show-manual');
47
- const manualConnect = document.getElementById('manual-connect');
48
- const agentDiscovery = document.getElementById('agent-discovery');
49
- const agentList = document.getElementById('agent-list');
50
44
 
51
45
  let ws;
52
46
  let peerConnection;
@@ -56,10 +50,24 @@ let AGENT_ID;
56
50
  let CLIENT_ID;
57
51
  let SELECTED_AGENT; // Store full agent data including WebSocket URL
58
52
 
59
- // Auto-discover agents on page load
53
+ // Check for agent parameter and connect directly
60
54
  window.addEventListener('load', () => {
61
- discoverAgents();
62
55
  loadVersionInfo();
56
+
57
+ // Get agent ID from URL parameter
58
+ const urlParams = new URLSearchParams(window.location.search);
59
+ const agentId = urlParams.get('agent');
60
+
61
+ if (agentId) {
62
+ AGENT_ID = agentId;
63
+ SELECTED_AGENT = { id: agentId, agentId: agentId };
64
+ console.log('[CLIENT] 🔗 Connecting directly to agent:', agentId);
65
+ startConnection();
66
+ } else {
67
+ // No agent specified, redirect to dashboard
68
+ console.log('[CLIENT] ❌ No agent specified, redirecting to dashboard');
69
+ window.location.href = '/app/dashboard.html';
70
+ }
63
71
  });
64
72
 
65
73
  // Load version info for footer
@@ -86,34 +94,7 @@ async function loadVersionInfo() {
86
94
  }
87
95
  }
88
96
 
89
- showManualBtn.onclick = () => {
90
- agentDiscovery.style.display = 'none';
91
- manualConnect.style.display = 'block';
92
- showManualBtn.style.display = 'none';
93
- };
94
-
95
- connectBtn.onclick = () => {
96
- AGENT_ID = agentIdInput.value.trim();
97
- if (!AGENT_ID) {
98
- alert('Please enter a valid Agent ID.');
99
- return;
100
- }
101
- startConnection();
102
- };
103
97
 
104
- function connectToAgent(agent) {
105
- if (typeof agent === 'string') {
106
- // Fallback for manual connection - agent is just the ID
107
- AGENT_ID = agent;
108
- SELECTED_AGENT = { id: agent, websocketUrl: null };
109
- } else {
110
- // Full agent object from discovery
111
- AGENT_ID = agent.id;
112
- SELECTED_AGENT = agent;
113
- }
114
- console.log('[CLIENT] 🔗 Connecting to agent:', SELECTED_AGENT);
115
- startConnection();
116
- }
117
98
 
118
99
  function startConnection() {
119
100
  connectContainer.style.display = 'none';
@@ -126,103 +107,6 @@ function startConnection() {
126
107
  initialize();
127
108
  }
128
109
 
129
- async function discoverAgents() {
130
- console.log('[DISCOVERY] 🔍 Starting agent discovery via PHP backend...');
131
- agentList.innerHTML = '<p style="color: #ccc;">Searching for Mac agents...</p>';
132
-
133
- try {
134
- // Use PHP backend for agent discovery instead of WebSocket
135
- const response = await fetch('/php-backend/api/list-agents.php', {
136
- method: 'GET',
137
- credentials: 'include', // Include session cookies for authentication
138
- headers: {
139
- 'Accept': 'application/json',
140
- 'Content-Type': 'application/json'
141
- }
142
- });
143
-
144
- if (!response.ok) {
145
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
146
- }
147
-
148
- const data = await response.json();
149
- console.log('[DISCOVERY] 📨 PHP Backend Response:', data);
150
-
151
- if (data.success && data.data && data.data.agents) {
152
- displayAvailableAgents(data.data.agents);
153
- } else {
154
- console.log('[DISCOVERY] ⚠️ No agents found in response');
155
- agentList.innerHTML = '<p style="color: #ff9800;">⚠️ No Mac agents found.<br><small>Make sure your Mac agent is running with: <code>shell-mirror</code></small></p>';
156
- showManualBtn.style.display = 'block';
157
- }
158
-
159
- } catch (error) {
160
- console.error('[DISCOVERY] ❌ PHP Backend error:', error);
161
-
162
- if (error.message.includes('401')) {
163
- agentList.innerHTML = '<p style="color: #f44336;">Authentication required. <a href="/php-backend/api/auth-login.php" style="color: #4CAF50;">Please log in</a></p>';
164
- } else {
165
- agentList.innerHTML = '<p style="color: #f44336;">Discovery failed. Check server connection.</p>';
166
- }
167
-
168
- showManualBtn.style.display = 'block';
169
- }
170
- }
171
-
172
- function displayAvailableAgents(agents) {
173
- console.log('[DISCOVERY] 🖥️ Displaying agents:', agents);
174
- agentList.innerHTML = '';
175
-
176
- if (agents.length === 0) {
177
- agentList.innerHTML = '<p style="color: #ff9800;">❌ No Mac agents currently running.</p>';
178
- showManualBtn.style.display = 'block';
179
- return;
180
- }
181
-
182
- console.log(`[DISCOVERY] ✅ Found ${agents.length} agent(s)`);
183
-
184
- agents.forEach(agent => {
185
- const agentDiv = document.createElement('div');
186
- agentDiv.style.cssText = 'margin: 10px 0; padding: 15px; background: #333; border-radius: 8px; cursor: pointer; border: 2px solid #555; transition: all 0.3s ease;';
187
- agentDiv.innerHTML = `
188
- <div style="display: flex; align-items: center; gap: 10px;">
189
- <div style="width: 12px; height: 12px; background: #4CAF50; border-radius: 50%; animation: pulse 2s infinite;"></div>
190
- <div>
191
- <strong style="color: #fff;">${agent.id}</strong><br>
192
- <small style="color: #aaa;">🖱️ Click to connect to Mac terminal</small>
193
- </div>
194
- </div>
195
- `;
196
-
197
- agentDiv.onmouseover = () => {
198
- agentDiv.style.borderColor = '#4CAF50';
199
- agentDiv.style.background = '#444';
200
- };
201
- agentDiv.onmouseout = () => {
202
- agentDiv.style.borderColor = '#555';
203
- agentDiv.style.background = '#333';
204
- };
205
- agentDiv.onclick = () => {
206
- console.log(`[DISCOVERY] 🖱️ User clicked on agent: ${agent.id}`, agent);
207
- connectToAgent(agent);
208
- };
209
-
210
- agentList.appendChild(agentDiv);
211
- });
212
-
213
- showManualBtn.style.display = 'block';
214
-
215
- // Add CSS animation for pulse effect
216
- const style = document.createElement('style');
217
- style.textContent = `
218
- @keyframes pulse {
219
- 0% { opacity: 1; }
220
- 50% { opacity: 0.5; }
221
- 100% { opacity: 1; }
222
- }
223
- `;
224
- document.head.appendChild(style);
225
- }
226
110
 
227
111
  async function initialize() {
228
112
  console.log('[CLIENT] 🚀 Initializing WebRTC connection to agent:', AGENT_ID);
package/public/index.html CHANGED
@@ -662,31 +662,30 @@
662
662
 
663
663
 
664
664
  <script>
665
- // Check if user is already authenticated and redirect to terminal
666
- async function checkAuthAndRedirect() {
665
+ // Check authentication status without auto-redirect
666
+ async function checkAuthStatus() {
667
667
  try {
668
668
  const response = await fetch('/php-backend/api/auth-status.php');
669
669
  const data = await response.json();
670
670
 
671
671
  if (data.success && data.data && data.data.authenticated) {
672
- // User is logged in, redirect to terminal
673
- window.location.href = '/app/terminal.html';
674
- return true;
672
+ return { isAuthenticated: true, user: data.data };
675
673
  }
676
674
  } catch (error) {
677
675
  console.log('Auth check failed:', error);
678
676
  }
679
- return false;
677
+ return { isAuthenticated: false, user: null };
680
678
  }
681
679
 
682
680
  // Handle Google login - direct web OAuth
683
681
  async function handleGoogleLogin() {
684
- // Check if already logged in first
685
- const isLoggedIn = await checkAuthAndRedirect();
686
- if (isLoggedIn) return;
687
-
688
682
  // Direct OAuth flow using the web backend
689
- window.location.href = '/php-backend/api/auth-login.php';
683
+ window.location.href = '/php-backend/api/auth-login.php?return=' + encodeURIComponent('/app/dashboard');
684
+ }
685
+
686
+ // Handle dashboard navigation
687
+ async function openDashboard() {
688
+ window.location.href = '/app/dashboard.html';
690
689
  }
691
690
 
692
691
  // Load version info
@@ -712,12 +711,43 @@
712
711
  }
713
712
  }
714
713
 
715
- // Check authentication status on page load
716
- document.addEventListener('DOMContentLoaded', () => {
717
- checkAuthAndRedirect();
714
+ // Initialize page on load
715
+ document.addEventListener('DOMContentLoaded', async () => {
716
+ await updateHeaderAndCTA();
718
717
  loadVersionInfo();
719
718
  });
720
719
 
720
+ // Update header and CTA based on auth status
721
+ async function updateHeaderAndCTA() {
722
+ const authStatus = await checkAuthStatus();
723
+
724
+ // Update header navigation
725
+ const headerNav = document.querySelector('nav');
726
+ if (authStatus.isAuthenticated) {
727
+ // Show user info + dashboard link
728
+ headerNav.innerHTML = `
729
+ <div class="logo">Shell Mirror</div>
730
+ <div style="display: flex; align-items: center; gap: 15px;">
731
+ <span style="color: white; opacity: 0.9;">Welcome, ${authStatus.user.name || authStatus.user.email}</span>
732
+ <a href="/app/dashboard.html" class="cta-button">Dashboard</a>
733
+ </div>
734
+ `;
735
+ } else {
736
+ // Show sign in option
737
+ headerNav.innerHTML = `
738
+ <div class="logo">Shell Mirror</div>
739
+ <button class="cta-button" onclick="handleGoogleLogin()">Sign In</button>
740
+ `;
741
+ }
742
+
743
+ // Update main CTA buttons
744
+ const primaryButtons = document.querySelectorAll('.btn-primary');
745
+ primaryButtons.forEach(button => {
746
+ button.textContent = 'Open Dashboard';
747
+ button.onclick = openDashboard;
748
+ });
749
+ }
750
+
721
751
  // Smooth scrolling for anchor links
722
752
  document.querySelectorAll('a[href^="#"]').forEach(anchor => {
723
753
  anchor.addEventListener('click', function (e) {