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 +2 -0
- package/package.json +1 -1
- package/public/app/dashboard.css +558 -0
- package/public/app/dashboard.html +76 -0
- package/public/app/dashboard.js +363 -0
- package/public/app/terminal.html +32 -11
- package/public/app/terminal.js +16 -132
- package/public/index.html +44 -14
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
|
@@ -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
|
+
});
|
package/public/app/terminal.html
CHANGED
|
@@ -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
|
-
<
|
|
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>
|
package/public/app/terminal.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
666
|
-
async function
|
|
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
|
-
|
|
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
|
-
//
|
|
716
|
-
document.addEventListener('DOMContentLoaded', () => {
|
|
717
|
-
|
|
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) {
|