@livedesk/client 0.1.51 → 0.1.53
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 +4 -0
- package/bin/livedesk-client.js +441 -65
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -13,6 +13,10 @@ ready yet, the client keeps checking every 5 seconds instead of exiting. Omit th
|
|
|
13
13
|
number for first-available placement, or pass `1` to `999` to pin this machine
|
|
14
14
|
to a screen wall slot.
|
|
15
15
|
|
|
16
|
+
On Windows, check **Start with Windows** on the connection page to reconnect this
|
|
17
|
+
client automatically after reboot. The startup entry reuses the saved Google
|
|
18
|
+
session and only opens the connection page again if sign-in is needed.
|
|
19
|
+
|
|
16
20
|
On the Hub computer, open the LiveDesk dashboard and sign in first. The
|
|
17
21
|
dashboard keeps its local Hub address and private pair token refreshed in the
|
|
18
22
|
LiveDesk Supabase registry for the signed-in account.
|
package/bin/livedesk-client.js
CHANGED
|
@@ -20,6 +20,8 @@ const DEFAULT_AUTH_CALLBACK_PORT = 5198;
|
|
|
20
20
|
const DISCOVERY_RETRY_MS = 5000;
|
|
21
21
|
const EXIT_INVALID_PAIR_TOKEN = 23;
|
|
22
22
|
const SESSION_REFRESH_SKEW_SECONDS = 60;
|
|
23
|
+
const WINDOWS_STARTUP_SCRIPT_NAME = 'LiveDesk Client.vbs';
|
|
24
|
+
const WINDOWS_STARTUP_LEGACY_CMD_NAME = 'LiveDesk Client.cmd';
|
|
23
25
|
const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngvjziawki.supabase.co';
|
|
24
26
|
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
25
27
|
const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
|
|
@@ -64,6 +66,7 @@ Options:
|
|
|
64
66
|
|
|
65
67
|
Auto uses C# RemoteFast when supported and falls back to Node for AI assist or
|
|
66
68
|
when a packaged RemoteFast runtime is unavailable.
|
|
69
|
+
Enable Windows auto-start from the connection page when this client signs in.
|
|
67
70
|
`.trimStart());
|
|
68
71
|
}
|
|
69
72
|
|
|
@@ -106,6 +109,7 @@ function parseLauncherArgs(argv) {
|
|
|
106
109
|
let command = 'connect';
|
|
107
110
|
let nodeOnlyFeature = false;
|
|
108
111
|
let fakeThumbnail = false;
|
|
112
|
+
let startupRun = false;
|
|
109
113
|
|
|
110
114
|
for (let index = 0; index < argv.length; index += 1) {
|
|
111
115
|
const arg = argv[index];
|
|
@@ -157,6 +161,10 @@ function parseLauncherArgs(argv) {
|
|
|
157
161
|
index += 1;
|
|
158
162
|
continue;
|
|
159
163
|
}
|
|
164
|
+
if (arg === '--startup-run') {
|
|
165
|
+
startupRun = true;
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
160
168
|
if (arg === '--manager') {
|
|
161
169
|
manager = argv[index + 1] || manager;
|
|
162
170
|
forwarded.push(arg, argv[index + 1]);
|
|
@@ -208,7 +216,8 @@ function parseLauncherArgs(argv) {
|
|
|
208
216
|
slot,
|
|
209
217
|
authPort,
|
|
210
218
|
nodeOnlyFeature,
|
|
211
|
-
fakeThumbnail
|
|
219
|
+
fakeThumbnail,
|
|
220
|
+
startupRun
|
|
212
221
|
};
|
|
213
222
|
}
|
|
214
223
|
|
|
@@ -249,6 +258,119 @@ function appendForwardedFlag(args, flag) {
|
|
|
249
258
|
return [...args, flag];
|
|
250
259
|
}
|
|
251
260
|
|
|
261
|
+
function getWindowsStartupDir() {
|
|
262
|
+
const appData = process.env.APPDATA || join(os.homedir(), 'AppData', 'Roaming');
|
|
263
|
+
return join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup');
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function getWindowsStartupScriptPath() {
|
|
267
|
+
return join(getWindowsStartupDir(), WINDOWS_STARTUP_SCRIPT_NAME);
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function getLegacyWindowsStartupCommandPath() {
|
|
271
|
+
return join(getWindowsStartupDir(), WINDOWS_STARTUP_LEGACY_CMD_NAME);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function isWindowsStartupSupported() {
|
|
275
|
+
return os.platform() === 'win32';
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function isWindowsStartupRegistered() {
|
|
279
|
+
return isWindowsStartupSupported() && existsSync(getWindowsStartupScriptPath());
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function quoteCommandArg(value) {
|
|
283
|
+
const text = String(value ?? '');
|
|
284
|
+
if (/^[A-Za-z0-9@._:/=+-]+$/.test(text)) {
|
|
285
|
+
return text;
|
|
286
|
+
}
|
|
287
|
+
return `"${text.replaceAll('"', '\\"')}"`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function escapeVbsString(value) {
|
|
291
|
+
return String(value ?? '').replaceAll('"', '""');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildStartupClientArgs(parsed) {
|
|
295
|
+
const args = [];
|
|
296
|
+
const slot = normalizeSlotNumber(parsed.slot);
|
|
297
|
+
if (slot) {
|
|
298
|
+
args.push(slot);
|
|
299
|
+
}
|
|
300
|
+
if (parsed.engine === 'fast') {
|
|
301
|
+
args.push('--fast');
|
|
302
|
+
} else if (parsed.engine === 'node') {
|
|
303
|
+
args.push('--node');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const forwarded = Array.isArray(parsed.forwarded) ? parsed.forwarded : [];
|
|
307
|
+
for (let index = 0; index < forwarded.length; index += 1) {
|
|
308
|
+
const arg = forwarded[index];
|
|
309
|
+
if (!arg || arg === 'connect') {
|
|
310
|
+
continue;
|
|
311
|
+
}
|
|
312
|
+
if (arg === '--slot' || arg === '--manager' || arg === '--pair' || arg === '--auth-port') {
|
|
313
|
+
index += 1;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
if (arg === '--login' || arg === '--logout' || arg === '--no-login' || arg === '--startup-run') {
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
args.push(arg);
|
|
320
|
+
}
|
|
321
|
+
args.push('--startup-run');
|
|
322
|
+
return args;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function registerWindowsStartup(startupArgs = []) {
|
|
326
|
+
if (!isWindowsStartupSupported()) {
|
|
327
|
+
return { changed: false, supported: false, path: '' };
|
|
328
|
+
}
|
|
329
|
+
const startupDir = getWindowsStartupDir();
|
|
330
|
+
const scriptPath = getWindowsStartupScriptPath();
|
|
331
|
+
const clientCommand = ['npx', '-y', '--prefer-online', 'livedesk@latest', 'client', ...startupArgs]
|
|
332
|
+
.filter(Boolean)
|
|
333
|
+
.map(quoteCommandArg)
|
|
334
|
+
.join(' ');
|
|
335
|
+
const runCommand = `cmd.exe /d /s /c "${clientCommand}"`;
|
|
336
|
+
const script = [
|
|
337
|
+
'Set shell = CreateObject("WScript.Shell")',
|
|
338
|
+
`shell.CurrentDirectory = "${escapeVbsString(os.homedir())}"`,
|
|
339
|
+
`shell.Run "${escapeVbsString(runCommand)}", 0, False`,
|
|
340
|
+
''
|
|
341
|
+
].join('\r\n');
|
|
342
|
+
|
|
343
|
+
mkdirSync(startupDir, { recursive: true });
|
|
344
|
+
writeFileSync(scriptPath, script, 'utf8');
|
|
345
|
+
rmSync(getLegacyWindowsStartupCommandPath(), { force: true });
|
|
346
|
+
return { changed: true, supported: true, path: scriptPath };
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function unregisterWindowsStartup() {
|
|
350
|
+
if (!isWindowsStartupSupported()) {
|
|
351
|
+
return { changed: false, supported: false, path: '' };
|
|
352
|
+
}
|
|
353
|
+
const scriptPath = getWindowsStartupScriptPath();
|
|
354
|
+
const existed = existsSync(scriptPath) || existsSync(getLegacyWindowsStartupCommandPath());
|
|
355
|
+
rmSync(scriptPath, { force: true });
|
|
356
|
+
rmSync(getLegacyWindowsStartupCommandPath(), { force: true });
|
|
357
|
+
return { changed: existed, supported: true, path: scriptPath };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function normalizeStartupChoice(value, fallback = false) {
|
|
361
|
+
if (value === null || value === undefined) {
|
|
362
|
+
return Boolean(fallback);
|
|
363
|
+
}
|
|
364
|
+
return /^(1|true|yes|on)$/i.test(String(value).trim());
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function applyStartupPreference(enabled, startupArgs = []) {
|
|
368
|
+
if (!isWindowsStartupSupported()) {
|
|
369
|
+
return { changed: false, supported: false, path: '' };
|
|
370
|
+
}
|
|
371
|
+
return enabled ? registerWindowsStartup(startupArgs) : unregisterWindowsStartup();
|
|
372
|
+
}
|
|
373
|
+
|
|
252
374
|
function createFileStorage(filePath) {
|
|
253
375
|
function readState() {
|
|
254
376
|
try {
|
|
@@ -428,10 +550,21 @@ function renderOAuthCallbackPage({ title, message, tone = 'neutral' }) {
|
|
|
428
550
|
</html>`;
|
|
429
551
|
}
|
|
430
552
|
|
|
431
|
-
function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
|
|
553
|
+
function renderConnectionChoicePage({ error = '', pin = '', slot = '', startup = false, startupSupported = false } = {}) {
|
|
432
554
|
const errorBlock = error
|
|
433
555
|
? `<div class="error">${escapeHtml(error)}</div>`
|
|
434
556
|
: '';
|
|
557
|
+
const normalizedSlot = normalizeSlotNumber(slot);
|
|
558
|
+
const slotLabel = normalizedSlot ? `Slot ${String(normalizedSlot).padStart(3, '0')}` : 'First available';
|
|
559
|
+
const startupBlock = startupSupported
|
|
560
|
+
? `<label class="startup-option">
|
|
561
|
+
<input id="startup-toggle" type="checkbox" ${startup ? 'checked' : ''}>
|
|
562
|
+
<span>
|
|
563
|
+
<strong>Start with Windows</strong>
|
|
564
|
+
<small>Reconnect automatically after reboot.</small>
|
|
565
|
+
</span>
|
|
566
|
+
</label>`
|
|
567
|
+
: '';
|
|
435
568
|
return `<!doctype html>
|
|
436
569
|
<html lang="en">
|
|
437
570
|
<head>
|
|
@@ -443,48 +576,132 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
|
|
|
443
576
|
* { box-sizing: border-box; }
|
|
444
577
|
html, body { width: 100%; min-height: 100%; margin: 0; }
|
|
445
578
|
body {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
padding: 24px;
|
|
449
|
-
background:
|
|
450
|
-
radial-gradient(circle at 50% 0%, rgba(148, 163, 184, 0.22), transparent 34%),
|
|
451
|
-
linear-gradient(180deg, #f8fafc, #eef2f7);
|
|
579
|
+
min-height: 100vh;
|
|
580
|
+
background: #f6f7f9;
|
|
452
581
|
color: #111827;
|
|
453
582
|
}
|
|
583
|
+
.shell {
|
|
584
|
+
min-height: 100vh;
|
|
585
|
+
display: grid;
|
|
586
|
+
grid-template-columns: minmax(360px, 462px) minmax(0, 1fr);
|
|
587
|
+
}
|
|
454
588
|
main {
|
|
455
|
-
|
|
589
|
+
display: flex;
|
|
590
|
+
flex-direction: column;
|
|
591
|
+
justify-content: center;
|
|
592
|
+
min-height: 100vh;
|
|
593
|
+
padding: 42px 54px;
|
|
594
|
+
border-right: 1px solid #d8dde5;
|
|
595
|
+
background: rgba(255, 255, 255, 0.96);
|
|
596
|
+
box-shadow: 18px 0 55px rgba(15, 23, 42, 0.08);
|
|
597
|
+
}
|
|
598
|
+
.stack {
|
|
599
|
+
width: min(100%, 386px);
|
|
456
600
|
display: grid;
|
|
457
|
-
gap:
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
601
|
+
gap: 24px;
|
|
602
|
+
}
|
|
603
|
+
.topbar {
|
|
604
|
+
position: absolute;
|
|
605
|
+
top: 28px;
|
|
606
|
+
left: 54px;
|
|
607
|
+
display: flex;
|
|
608
|
+
align-items: center;
|
|
609
|
+
gap: 11px;
|
|
463
610
|
}
|
|
464
611
|
.brand {
|
|
465
612
|
display: flex;
|
|
466
613
|
align-items: center;
|
|
467
|
-
gap:
|
|
614
|
+
gap: 13px;
|
|
468
615
|
}
|
|
469
616
|
.mark {
|
|
470
|
-
width:
|
|
471
|
-
height:
|
|
617
|
+
width: 38px;
|
|
618
|
+
height: 38px;
|
|
472
619
|
display: grid;
|
|
473
620
|
place-items: center;
|
|
474
|
-
border-radius:
|
|
475
|
-
background: #
|
|
621
|
+
border-radius: 9px;
|
|
622
|
+
background: linear-gradient(135deg, #2dd4bf, #2563eb);
|
|
476
623
|
color: #ffffff;
|
|
477
624
|
font-weight: 950;
|
|
625
|
+
box-shadow: 0 12px 28px rgba(37, 99, 235, 0.18);
|
|
478
626
|
}
|
|
479
|
-
|
|
480
|
-
|
|
627
|
+
.brand-copy {
|
|
628
|
+
display: grid;
|
|
629
|
+
gap: 2px;
|
|
630
|
+
line-height: 1;
|
|
631
|
+
}
|
|
632
|
+
.brand-copy strong {
|
|
633
|
+
font-size: 15px;
|
|
634
|
+
letter-spacing: 0;
|
|
635
|
+
}
|
|
636
|
+
.brand-copy span {
|
|
637
|
+
color: #64748b;
|
|
638
|
+
font-size: 12px;
|
|
639
|
+
font-weight: 750;
|
|
640
|
+
}
|
|
641
|
+
.headline {
|
|
642
|
+
display: grid;
|
|
643
|
+
gap: 9px;
|
|
644
|
+
padding-top: 16px;
|
|
645
|
+
}
|
|
646
|
+
h1 { margin: 0; font-size: 32px; line-height: 1.08; letter-spacing: 0; }
|
|
647
|
+
p { margin: 0; color: #64748b; font-size: 14px; line-height: 1.55; font-weight: 650; }
|
|
481
648
|
.panel {
|
|
482
649
|
display: grid;
|
|
483
|
-
gap:
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
650
|
+
gap: 14px;
|
|
651
|
+
}
|
|
652
|
+
.placement {
|
|
653
|
+
display: flex;
|
|
654
|
+
align-items: center;
|
|
655
|
+
justify-content: space-between;
|
|
656
|
+
min-height: 38px;
|
|
657
|
+
padding: 0 12px;
|
|
658
|
+
border: 1px solid #d9dee7;
|
|
659
|
+
border-radius: 8px;
|
|
660
|
+
background: #ffffff;
|
|
661
|
+
box-shadow: 0 1px 2px rgba(15, 23, 42, 0.04);
|
|
662
|
+
}
|
|
663
|
+
.placement span {
|
|
664
|
+
color: #64748b;
|
|
665
|
+
font-size: 12px;
|
|
666
|
+
font-weight: 800;
|
|
667
|
+
}
|
|
668
|
+
.placement strong {
|
|
669
|
+
color: #111827;
|
|
670
|
+
font-size: 13px;
|
|
671
|
+
font-weight: 900;
|
|
672
|
+
}
|
|
673
|
+
.startup-option {
|
|
674
|
+
display: grid;
|
|
675
|
+
grid-template-columns: 18px 1fr;
|
|
676
|
+
gap: 10px;
|
|
677
|
+
align-items: center;
|
|
678
|
+
padding: 11px 12px;
|
|
679
|
+
border: 1px solid #d9dee7;
|
|
680
|
+
border-radius: 8px;
|
|
487
681
|
background: #ffffff;
|
|
682
|
+
color: #111827;
|
|
683
|
+
cursor: pointer;
|
|
684
|
+
user-select: none;
|
|
685
|
+
}
|
|
686
|
+
.startup-option input {
|
|
687
|
+
width: 16px;
|
|
688
|
+
height: 16px;
|
|
689
|
+
margin: 0;
|
|
690
|
+
accent-color: #111827;
|
|
691
|
+
}
|
|
692
|
+
.startup-option span {
|
|
693
|
+
display: grid;
|
|
694
|
+
gap: 2px;
|
|
695
|
+
}
|
|
696
|
+
.startup-option strong {
|
|
697
|
+
font-size: 13px;
|
|
698
|
+
font-weight: 900;
|
|
699
|
+
}
|
|
700
|
+
.startup-option small {
|
|
701
|
+
color: #64748b;
|
|
702
|
+
text-align: left;
|
|
703
|
+
font-size: 12px;
|
|
704
|
+
font-weight: 700;
|
|
488
705
|
}
|
|
489
706
|
form {
|
|
490
707
|
display: grid;
|
|
@@ -501,12 +718,13 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
|
|
|
501
718
|
letter-spacing: 0.04em;
|
|
502
719
|
}
|
|
503
720
|
input {
|
|
504
|
-
height:
|
|
505
|
-
border: 1px solid
|
|
506
|
-
border-radius:
|
|
507
|
-
padding: 0
|
|
721
|
+
height: 42px;
|
|
722
|
+
border: 1px solid #cfd6e1;
|
|
723
|
+
border-radius: 8px;
|
|
724
|
+
padding: 0 12px;
|
|
725
|
+
background: rgba(17, 24, 39, 0.026);
|
|
508
726
|
color: #111827;
|
|
509
|
-
font-size:
|
|
727
|
+
font-size: 22px;
|
|
510
728
|
font-weight: 950;
|
|
511
729
|
letter-spacing: 0.18em;
|
|
512
730
|
text-align: center;
|
|
@@ -517,25 +735,36 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
|
|
|
517
735
|
box-shadow: 0 0 0 3px rgba(17, 24, 39, 0.08);
|
|
518
736
|
}
|
|
519
737
|
button, .google {
|
|
520
|
-
height:
|
|
738
|
+
height: 42px;
|
|
521
739
|
display: inline-flex;
|
|
522
740
|
align-items: center;
|
|
523
741
|
justify-content: center;
|
|
524
742
|
gap: 8px;
|
|
525
|
-
border:
|
|
526
|
-
border-radius: 12px;
|
|
743
|
+
border-radius: 8px;
|
|
527
744
|
background: #111827;
|
|
528
745
|
color: #ffffff;
|
|
529
746
|
font-size: 14px;
|
|
530
747
|
font-weight: 900;
|
|
531
748
|
text-decoration: none;
|
|
532
749
|
cursor: pointer;
|
|
750
|
+
transition: border-color 160ms ease, background 160ms ease, transform 160ms ease, box-shadow 160ms ease;
|
|
751
|
+
}
|
|
752
|
+
button {
|
|
753
|
+
border: 1px solid #111827;
|
|
754
|
+
box-shadow: 0 10px 24px rgba(17, 24, 39, 0.12);
|
|
755
|
+
}
|
|
756
|
+
button:hover, .google:hover {
|
|
757
|
+
transform: translateY(-1px);
|
|
533
758
|
}
|
|
534
759
|
.google {
|
|
535
760
|
background: #ffffff;
|
|
536
|
-
border: 1px solid
|
|
761
|
+
border: 1px solid #cfd6e1;
|
|
537
762
|
color: #111827;
|
|
538
763
|
}
|
|
764
|
+
.google:hover {
|
|
765
|
+
border-color: #111827;
|
|
766
|
+
box-shadow: 0 10px 24px rgba(15, 23, 42, 0.08);
|
|
767
|
+
}
|
|
539
768
|
.divider {
|
|
540
769
|
display: grid;
|
|
541
770
|
grid-template-columns: 1fr auto 1fr;
|
|
@@ -550,11 +779,11 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
|
|
|
550
779
|
.divider::before, .divider::after {
|
|
551
780
|
content: "";
|
|
552
781
|
height: 1px;
|
|
553
|
-
background:
|
|
782
|
+
background: #d9dee7;
|
|
554
783
|
}
|
|
555
784
|
.error {
|
|
556
|
-
border: 1px solid rgba(220, 38, 38, 0.
|
|
557
|
-
border-radius:
|
|
785
|
+
border: 1px solid rgba(220, 38, 38, 0.24);
|
|
786
|
+
border-radius: 8px;
|
|
558
787
|
padding: 10px 12px;
|
|
559
788
|
background: rgba(254, 226, 226, 0.75);
|
|
560
789
|
color: #991b1b;
|
|
@@ -567,31 +796,150 @@ function renderConnectionChoicePage({ error = '', pin = '' } = {}) {
|
|
|
567
796
|
font-size: 12px;
|
|
568
797
|
font-weight: 750;
|
|
569
798
|
}
|
|
799
|
+
aside {
|
|
800
|
+
display: grid;
|
|
801
|
+
place-items: center;
|
|
802
|
+
min-height: 100vh;
|
|
803
|
+
padding: 56px;
|
|
804
|
+
background:
|
|
805
|
+
radial-gradient(circle at 58% 28%, rgba(203, 213, 225, 0.5), transparent 28%),
|
|
806
|
+
linear-gradient(180deg, #f8fafc, #eef2f7);
|
|
807
|
+
}
|
|
808
|
+
.preview {
|
|
809
|
+
width: min(560px, 90%);
|
|
810
|
+
display: grid;
|
|
811
|
+
gap: 14px;
|
|
812
|
+
padding: 18px;
|
|
813
|
+
border: 1px solid rgba(203, 213, 225, 0.76);
|
|
814
|
+
border-radius: 14px;
|
|
815
|
+
background: rgba(255, 255, 255, 0.76);
|
|
816
|
+
box-shadow: 0 28px 90px rgba(15, 23, 42, 0.13);
|
|
817
|
+
}
|
|
818
|
+
.preview-title {
|
|
819
|
+
display: flex;
|
|
820
|
+
align-items: center;
|
|
821
|
+
justify-content: space-between;
|
|
822
|
+
color: #475569;
|
|
823
|
+
font-size: 12px;
|
|
824
|
+
font-weight: 900;
|
|
825
|
+
text-transform: uppercase;
|
|
826
|
+
letter-spacing: 0.06em;
|
|
827
|
+
}
|
|
828
|
+
.preview-dot {
|
|
829
|
+
width: 9px;
|
|
830
|
+
height: 9px;
|
|
831
|
+
border-radius: 999px;
|
|
832
|
+
background: #14b8a6;
|
|
833
|
+
box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.12);
|
|
834
|
+
}
|
|
835
|
+
.screen-grid {
|
|
836
|
+
display: grid;
|
|
837
|
+
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
838
|
+
gap: 12px;
|
|
839
|
+
}
|
|
840
|
+
.screen {
|
|
841
|
+
aspect-ratio: 16 / 9;
|
|
842
|
+
border: 1px solid #d8dee8;
|
|
843
|
+
border-radius: 8px;
|
|
844
|
+
background:
|
|
845
|
+
linear-gradient(rgba(148, 163, 184, 0.11) 1px, transparent 1px),
|
|
846
|
+
linear-gradient(90deg, rgba(148, 163, 184, 0.11) 1px, transparent 1px),
|
|
847
|
+
#ffffff;
|
|
848
|
+
background-size: 32px 32px;
|
|
849
|
+
box-shadow: inset 0 -16px 26px rgba(15, 23, 42, 0.04);
|
|
850
|
+
}
|
|
851
|
+
.caption {
|
|
852
|
+
color: #64748b;
|
|
853
|
+
font-size: 13px;
|
|
854
|
+
font-weight: 700;
|
|
855
|
+
line-height: 1.55;
|
|
856
|
+
}
|
|
857
|
+
@media (max-width: 860px) {
|
|
858
|
+
.shell { grid-template-columns: 1fr; }
|
|
859
|
+
main {
|
|
860
|
+
min-height: 100vh;
|
|
861
|
+
padding: 84px 28px 36px;
|
|
862
|
+
border-right: 0;
|
|
863
|
+
}
|
|
864
|
+
.topbar {
|
|
865
|
+
left: 28px;
|
|
866
|
+
top: 24px;
|
|
867
|
+
}
|
|
868
|
+
.stack { justify-self: center; }
|
|
869
|
+
aside { display: none; }
|
|
870
|
+
}
|
|
570
871
|
</style>
|
|
571
872
|
</head>
|
|
572
873
|
<body>
|
|
573
|
-
<
|
|
574
|
-
<
|
|
575
|
-
<div class="
|
|
576
|
-
|
|
577
|
-
<
|
|
578
|
-
|
|
874
|
+
<div class="shell">
|
|
875
|
+
<main>
|
|
876
|
+
<div class="topbar">
|
|
877
|
+
<div class="mark">LD</div>
|
|
878
|
+
<div class="brand-copy">
|
|
879
|
+
<strong>LiveDesk</strong>
|
|
880
|
+
<span>Client connect</span>
|
|
881
|
+
</div>
|
|
579
882
|
</div>
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
883
|
+
<div class="stack">
|
|
884
|
+
<div class="headline">
|
|
885
|
+
<h1>Connect this computer</h1>
|
|
886
|
+
<p>Use the same Google account as your Hub, or enter the 6-digit Hub PIN from the screen wall.</p>
|
|
887
|
+
</div>
|
|
888
|
+
${errorBlock}
|
|
889
|
+
<section class="panel">
|
|
890
|
+
<form id="google-form" method="get" action="/google">
|
|
891
|
+
<input id="google-startup" type="hidden" name="startup" value="${startup ? '1' : '0'}">
|
|
892
|
+
<button class="google" type="submit">Continue with Google</button>
|
|
893
|
+
</form>
|
|
894
|
+
<div class="divider">or</div>
|
|
895
|
+
<form id="pin-form" method="post" action="/pin">
|
|
896
|
+
<input id="pin-startup" type="hidden" name="startup" value="${startup ? '1' : '0'}">
|
|
897
|
+
<label>
|
|
898
|
+
Hub PIN
|
|
899
|
+
<input name="pin" value="${escapeHtml(pin)}" inputmode="numeric" autocomplete="one-time-code" pattern="[0-9]{6}" maxlength="6" placeholder="000000" autofocus>
|
|
900
|
+
</label>
|
|
901
|
+
<button type="submit">Connect with PIN</button>
|
|
902
|
+
</form>
|
|
903
|
+
${startupBlock}
|
|
904
|
+
<div class="placement">
|
|
905
|
+
<span>Placement</span>
|
|
906
|
+
<strong>${escapeHtml(slotLabel)}</strong>
|
|
907
|
+
</div>
|
|
908
|
+
</section>
|
|
909
|
+
<small>This tab can stay open until the connection is ready.</small>
|
|
910
|
+
</div>
|
|
911
|
+
</main>
|
|
912
|
+
<aside aria-hidden="true">
|
|
913
|
+
<div class="preview">
|
|
914
|
+
<div class="preview-title">
|
|
915
|
+
<span>Screen wall ready</span>
|
|
916
|
+
<span class="preview-dot"></span>
|
|
917
|
+
</div>
|
|
918
|
+
<div class="screen-grid">
|
|
919
|
+
<div class="screen"></div>
|
|
920
|
+
<div class="screen"></div>
|
|
921
|
+
<div class="screen"></div>
|
|
922
|
+
<div class="screen"></div>
|
|
923
|
+
</div>
|
|
924
|
+
<div class="caption">LiveDesk places this computer on the Hub wall after sign-in or PIN approval.</div>
|
|
925
|
+
</div>
|
|
926
|
+
</aside>
|
|
927
|
+
</div>
|
|
928
|
+
<script>
|
|
929
|
+
const startupToggle = document.getElementById('startup-toggle');
|
|
930
|
+
const startupFields = [
|
|
931
|
+
document.getElementById('google-startup'),
|
|
932
|
+
document.getElementById('pin-startup')
|
|
933
|
+
].filter(Boolean);
|
|
934
|
+
function syncStartupFields() {
|
|
935
|
+
const value = startupToggle && startupToggle.checked ? '1' : '0';
|
|
936
|
+
startupFields.forEach(field => { field.value = value; });
|
|
937
|
+
}
|
|
938
|
+
if (startupToggle) {
|
|
939
|
+
startupToggle.addEventListener('change', syncStartupFields);
|
|
940
|
+
syncStartupFields();
|
|
941
|
+
}
|
|
942
|
+
</script>
|
|
595
943
|
</body>
|
|
596
944
|
</html>`;
|
|
597
945
|
}
|
|
@@ -655,6 +1003,9 @@ async function resolveManagerFromPin(supabase, pin) {
|
|
|
655
1003
|
async function startConnectionChoiceServer(supabase, options = {}) {
|
|
656
1004
|
const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
|
|
657
1005
|
const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
|
|
1006
|
+
const slot = normalizeSlotNumber(options.slot);
|
|
1007
|
+
const startupArgs = Array.isArray(options.startupArgs) ? options.startupArgs : [];
|
|
1008
|
+
let pendingStartup = isWindowsStartupRegistered();
|
|
658
1009
|
let listeningPort = port;
|
|
659
1010
|
let completed = false;
|
|
660
1011
|
let completedTitle = 'LiveDesk connection complete';
|
|
@@ -673,10 +1024,16 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
673
1024
|
settleChoice(choice);
|
|
674
1025
|
setImmediate(() => server.close());
|
|
675
1026
|
};
|
|
1027
|
+
const renderChoice = (props = {}) => renderConnectionChoicePage({
|
|
1028
|
+
slot,
|
|
1029
|
+
startup: pendingStartup,
|
|
1030
|
+
startupSupported: isWindowsStartupSupported(),
|
|
1031
|
+
...props
|
|
1032
|
+
});
|
|
676
1033
|
|
|
677
1034
|
const handleError = (res, error, pin = '') => {
|
|
678
1035
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
679
|
-
res.end(
|
|
1036
|
+
res.end(renderChoice({
|
|
680
1037
|
error: error instanceof Error ? error.message : String(error),
|
|
681
1038
|
pin
|
|
682
1039
|
}));
|
|
@@ -701,13 +1058,15 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
701
1058
|
|
|
702
1059
|
if (requestUrl.pathname === '/' || requestUrl.pathname === '/connect') {
|
|
703
1060
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
704
|
-
res.end(
|
|
1061
|
+
res.end(renderChoice());
|
|
705
1062
|
return;
|
|
706
1063
|
}
|
|
707
1064
|
|
|
708
1065
|
if (requestUrl.pathname === '/google') {
|
|
1066
|
+
pendingStartup = normalizeStartupChoice(requestUrl.searchParams.get('startup'), pendingStartup);
|
|
709
1067
|
const { data: existing } = await supabase.auth.getSession();
|
|
710
1068
|
if (existing?.session?.access_token) {
|
|
1069
|
+
applyStartupPreference(pendingStartup, startupArgs);
|
|
711
1070
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
712
1071
|
res.end(renderOAuthCallbackPage({
|
|
713
1072
|
title: 'LiveDesk sign-in ready',
|
|
@@ -739,14 +1098,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
739
1098
|
|
|
740
1099
|
if (requestUrl.pathname === '/pin') {
|
|
741
1100
|
let pin = requestUrl.searchParams.get('pin') || '';
|
|
1101
|
+
let startupValue = requestUrl.searchParams.get('startup');
|
|
742
1102
|
if (req.method !== 'GET') {
|
|
743
1103
|
const body = await readRequestBody(req);
|
|
744
1104
|
const params = new URLSearchParams(body);
|
|
745
1105
|
pin = params.get('pin') || pin;
|
|
1106
|
+
startupValue = params.get('startup');
|
|
746
1107
|
}
|
|
1108
|
+
pendingStartup = normalizeStartupChoice(startupValue, pendingStartup);
|
|
747
1109
|
pin = normalizePairingPin(pin);
|
|
748
1110
|
try {
|
|
749
1111
|
const resolved = await resolveManagerFromPin(supabase, pin);
|
|
1112
|
+
applyStartupPreference(pendingStartup, startupArgs);
|
|
750
1113
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
751
1114
|
res.end(renderOAuthCallbackPage({
|
|
752
1115
|
title: 'LiveDesk PIN accepted',
|
|
@@ -776,7 +1139,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
776
1139
|
}
|
|
777
1140
|
if (!code) {
|
|
778
1141
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
779
|
-
res.end(
|
|
1142
|
+
res.end(renderChoice({
|
|
780
1143
|
error: 'Google did not return an auth code. Try Google sign-in again.'
|
|
781
1144
|
}));
|
|
782
1145
|
return;
|
|
@@ -795,6 +1158,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
795
1158
|
server.close();
|
|
796
1159
|
return;
|
|
797
1160
|
}
|
|
1161
|
+
applyStartupPreference(pendingStartup, startupArgs);
|
|
798
1162
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
799
1163
|
res.end(renderOAuthCallbackPage({
|
|
800
1164
|
title: 'LiveDesk sign-in complete',
|
|
@@ -805,7 +1169,7 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
805
1169
|
}
|
|
806
1170
|
|
|
807
1171
|
res.writeHead(404, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
808
|
-
res.end(
|
|
1172
|
+
res.end(renderChoice({
|
|
809
1173
|
error: 'Unknown LiveDesk connection route.'
|
|
810
1174
|
}));
|
|
811
1175
|
})().catch(err => {
|
|
@@ -846,7 +1210,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
846
1210
|
}
|
|
847
1211
|
|
|
848
1212
|
async function chooseClientConnection(supabase, options = {}) {
|
|
849
|
-
const connectionPage = await startConnectionChoiceServer(supabase, {
|
|
1213
|
+
const connectionPage = await startConnectionChoiceServer(supabase, {
|
|
1214
|
+
authPort: options.authPort,
|
|
1215
|
+
slot: options.slot,
|
|
1216
|
+
startupArgs: options.startupArgs
|
|
1217
|
+
});
|
|
850
1218
|
console.log('Opening LiveDesk connection page...');
|
|
851
1219
|
openBrowser(connectionPage.url);
|
|
852
1220
|
return connectionPage.waitForChoice;
|
|
@@ -976,7 +1344,15 @@ async function prepareLoginConnection(parsed) {
|
|
|
976
1344
|
|
|
977
1345
|
if (shouldLogin) {
|
|
978
1346
|
const supabase = await createSupabaseClient();
|
|
979
|
-
const
|
|
1347
|
+
const startupArgs = buildStartupClientArgs(parsed);
|
|
1348
|
+
const savedSession = parsed.startupRun ? await refreshSessionIfNeeded(supabase) : null;
|
|
1349
|
+
const choice = savedSession?.access_token
|
|
1350
|
+
? { type: 'google', session: savedSession }
|
|
1351
|
+
: await chooseClientConnection(supabase, {
|
|
1352
|
+
authPort: parsed.authPort,
|
|
1353
|
+
slot: parsed.slot,
|
|
1354
|
+
startupArgs
|
|
1355
|
+
});
|
|
980
1356
|
if (choice.type === 'pin') {
|
|
981
1357
|
manager = choice.manager;
|
|
982
1358
|
pair = choice.pair;
|