@livedesk/client 0.1.56 → 0.1.58
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/bin/livedesk-client.js +569 -37
- package/package.json +1 -1
package/bin/livedesk-client.js
CHANGED
|
@@ -26,6 +26,7 @@ const SUPABASE_URL = process.env.LIVEDESK_SUPABASE_URL || 'https://otbyfkjxrkngv
|
|
|
26
26
|
const SUPABASE_PUBLISHABLE_KEY = process.env.LIVEDESK_SUPABASE_PUBLISHABLE_KEY || 'sb_publishable_NpUs0RDJH2YnllsqTKO6TQ_1jTdSsNQ';
|
|
27
27
|
const CLIENT_STATE_DIR = join(os.homedir(), '.livedesk-client');
|
|
28
28
|
const CLIENT_AUTH_PATH = join(CLIENT_STATE_DIR, 'auth.json');
|
|
29
|
+
const CLIENT_PIN_PATH = join(CLIENT_STATE_DIR, 'pin.json');
|
|
29
30
|
const CLIENT_AUTH_STORAGE_KEY = 'livedesk.client.supabase.auth';
|
|
30
31
|
|
|
31
32
|
function printHelp() {
|
|
@@ -38,6 +39,8 @@ Usage:
|
|
|
38
39
|
|
|
39
40
|
Default flow:
|
|
40
41
|
Opens a LiveDesk connection page with Google sign-in or a 6-digit PIN.
|
|
42
|
+
After auth, that page stays open as this computer's client dashboard.
|
|
43
|
+
Saved Google sign-in or saved PIN starts the client automatically.
|
|
41
44
|
Omit the number for first-available placement, or pass 1-999 to pin a slot.
|
|
42
45
|
If no LiveDesk Hub is active yet, the client keeps checking every 5 seconds.
|
|
43
46
|
|
|
@@ -940,6 +943,332 @@ function renderConnectionChoicePage({ error = '', pin = '', slot = '', startup =
|
|
|
940
943
|
startupToggle.addEventListener('change', syncStartupFields);
|
|
941
944
|
syncStartupFields();
|
|
942
945
|
}
|
|
946
|
+
async function watchAutoConnection() {
|
|
947
|
+
try {
|
|
948
|
+
const response = await fetch('/api/state', { cache: 'no-store' });
|
|
949
|
+
if (!response.ok) return;
|
|
950
|
+
const state = await response.json();
|
|
951
|
+
if (state.completed) {
|
|
952
|
+
location.replace('/');
|
|
953
|
+
}
|
|
954
|
+
} catch {
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
setInterval(watchAutoConnection, 800);
|
|
958
|
+
watchAutoConnection();
|
|
959
|
+
</script>
|
|
960
|
+
</body>
|
|
961
|
+
</html>`;
|
|
962
|
+
}
|
|
963
|
+
|
|
964
|
+
function getChoiceEmail(choice) {
|
|
965
|
+
return choice?.session?.user?.email || '';
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function getDashboardAuthLabel(choice, loggedOut = false) {
|
|
969
|
+
if (loggedOut) {
|
|
970
|
+
return 'Signed out';
|
|
971
|
+
}
|
|
972
|
+
if (choice?.type === 'pin') {
|
|
973
|
+
return 'PIN approved';
|
|
974
|
+
}
|
|
975
|
+
const email = getChoiceEmail(choice);
|
|
976
|
+
return email ? `Google - ${email}` : 'Google signed in';
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function renderConnectionDashboardPage(state = {}) {
|
|
980
|
+
const choice = state.choice || null;
|
|
981
|
+
const connectedAt = state.connectedAt || new Date().toISOString();
|
|
982
|
+
const authLabel = getDashboardAuthLabel(choice, state.loggedOut);
|
|
983
|
+
const manager = state.manager || (choice?.manager || '');
|
|
984
|
+
const slot = normalizeSlotNumber(state.slot) ? `Slot ${String(state.slot).padStart(3, '0')}` : 'First available';
|
|
985
|
+
const statusLabel = state.loggedOut
|
|
986
|
+
? 'Signed out for next restart'
|
|
987
|
+
: manager
|
|
988
|
+
? 'Client starting'
|
|
989
|
+
: 'Finding Hub';
|
|
990
|
+
const message = state.message || (manager
|
|
991
|
+
? 'The LiveDesk client is running from the terminal. Keep this tab open as the local client dashboard.'
|
|
992
|
+
: 'Sign-in is complete. The client is looking for the active LiveDesk Hub and will start automatically.');
|
|
993
|
+
const stateJson = JSON.stringify({
|
|
994
|
+
authLabel,
|
|
995
|
+
connectedAt,
|
|
996
|
+
manager,
|
|
997
|
+
message,
|
|
998
|
+
slot,
|
|
999
|
+
startup: Boolean(state.startup),
|
|
1000
|
+
completed: true,
|
|
1001
|
+
statusLabel
|
|
1002
|
+
}).replaceAll('<', '\\u003c');
|
|
1003
|
+
|
|
1004
|
+
return `<!doctype html>
|
|
1005
|
+
<html lang="en">
|
|
1006
|
+
<head>
|
|
1007
|
+
<meta charset="utf-8">
|
|
1008
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
1009
|
+
<title>LiveDesk Client Dashboard</title>
|
|
1010
|
+
<style>
|
|
1011
|
+
:root { color-scheme: light; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
|
|
1012
|
+
* { box-sizing: border-box; }
|
|
1013
|
+
html, body { width: 100%; min-height: 100%; margin: 0; }
|
|
1014
|
+
body {
|
|
1015
|
+
min-height: 100vh;
|
|
1016
|
+
display: grid;
|
|
1017
|
+
place-items: center;
|
|
1018
|
+
padding: 28px;
|
|
1019
|
+
background:
|
|
1020
|
+
radial-gradient(circle at 78% 10%, rgba(203, 213, 225, 0.46), transparent 24%),
|
|
1021
|
+
linear-gradient(180deg, #f8fafc, #eef2f7);
|
|
1022
|
+
color: #111827;
|
|
1023
|
+
}
|
|
1024
|
+
main {
|
|
1025
|
+
width: min(920px, 100%);
|
|
1026
|
+
display: grid;
|
|
1027
|
+
grid-template-columns: minmax(280px, 0.88fr) minmax(320px, 1.12fr);
|
|
1028
|
+
overflow: hidden;
|
|
1029
|
+
border: 1px solid rgba(148, 163, 184, 0.28);
|
|
1030
|
+
border-radius: 18px;
|
|
1031
|
+
background: rgba(255, 255, 255, 0.92);
|
|
1032
|
+
box-shadow: 0 36px 110px rgba(15, 23, 42, 0.14);
|
|
1033
|
+
}
|
|
1034
|
+
.hero {
|
|
1035
|
+
display: grid;
|
|
1036
|
+
align-content: space-between;
|
|
1037
|
+
gap: 34px;
|
|
1038
|
+
padding: 34px;
|
|
1039
|
+
border-right: 1px solid rgba(148, 163, 184, 0.22);
|
|
1040
|
+
background:
|
|
1041
|
+
linear-gradient(145deg, rgba(255, 255, 255, 0.9), rgba(248, 250, 252, 0.86)),
|
|
1042
|
+
#ffffff;
|
|
1043
|
+
}
|
|
1044
|
+
.brand {
|
|
1045
|
+
display: flex;
|
|
1046
|
+
align-items: center;
|
|
1047
|
+
gap: 13px;
|
|
1048
|
+
}
|
|
1049
|
+
.mark {
|
|
1050
|
+
width: 40px;
|
|
1051
|
+
height: 40px;
|
|
1052
|
+
display: grid;
|
|
1053
|
+
place-items: center;
|
|
1054
|
+
border-radius: 10px;
|
|
1055
|
+
background: linear-gradient(135deg, #2dd4bf, #2563eb);
|
|
1056
|
+
color: #ffffff;
|
|
1057
|
+
font-size: 13px;
|
|
1058
|
+
font-weight: 800;
|
|
1059
|
+
box-shadow: 0 14px 30px rgba(37, 99, 235, 0.18);
|
|
1060
|
+
}
|
|
1061
|
+
.brand-copy { display: grid; gap: 2px; line-height: 1; }
|
|
1062
|
+
.brand-copy strong { font-size: 15px; font-weight: 700; }
|
|
1063
|
+
.brand-copy span { color: #64748b; font-size: 12px; font-weight: 500; }
|
|
1064
|
+
.headline { display: grid; gap: 10px; }
|
|
1065
|
+
.eyebrow {
|
|
1066
|
+
color: #475569;
|
|
1067
|
+
font-size: 11px;
|
|
1068
|
+
font-weight: 800;
|
|
1069
|
+
letter-spacing: 0.08em;
|
|
1070
|
+
text-transform: uppercase;
|
|
1071
|
+
}
|
|
1072
|
+
h1 {
|
|
1073
|
+
margin: 0;
|
|
1074
|
+
font-size: 34px;
|
|
1075
|
+
line-height: 1.06;
|
|
1076
|
+
font-weight: 680;
|
|
1077
|
+
letter-spacing: 0;
|
|
1078
|
+
}
|
|
1079
|
+
p {
|
|
1080
|
+
margin: 0;
|
|
1081
|
+
color: #64748b;
|
|
1082
|
+
font-size: 14px;
|
|
1083
|
+
line-height: 1.62;
|
|
1084
|
+
font-weight: 450;
|
|
1085
|
+
}
|
|
1086
|
+
.panel {
|
|
1087
|
+
display: grid;
|
|
1088
|
+
gap: 16px;
|
|
1089
|
+
padding: 34px;
|
|
1090
|
+
}
|
|
1091
|
+
.status-card {
|
|
1092
|
+
display: grid;
|
|
1093
|
+
gap: 14px;
|
|
1094
|
+
padding: 18px;
|
|
1095
|
+
border: 1px solid rgba(148, 163, 184, 0.24);
|
|
1096
|
+
border-radius: 14px;
|
|
1097
|
+
background: #ffffff;
|
|
1098
|
+
box-shadow:
|
|
1099
|
+
0 20px 50px rgba(15, 23, 42, 0.09),
|
|
1100
|
+
0 1px 0 rgba(255, 255, 255, 0.9) inset;
|
|
1101
|
+
}
|
|
1102
|
+
.status-head {
|
|
1103
|
+
display: flex;
|
|
1104
|
+
align-items: center;
|
|
1105
|
+
justify-content: space-between;
|
|
1106
|
+
gap: 12px;
|
|
1107
|
+
}
|
|
1108
|
+
.status-pill {
|
|
1109
|
+
min-height: 28px;
|
|
1110
|
+
display: inline-flex;
|
|
1111
|
+
align-items: center;
|
|
1112
|
+
gap: 7px;
|
|
1113
|
+
padding: 0 10px;
|
|
1114
|
+
border: 1px solid rgba(20, 184, 166, 0.25);
|
|
1115
|
+
border-radius: 999px;
|
|
1116
|
+
background: rgba(20, 184, 166, 0.08);
|
|
1117
|
+
color: #0f766e;
|
|
1118
|
+
font-size: 12px;
|
|
1119
|
+
font-weight: 760;
|
|
1120
|
+
}
|
|
1121
|
+
.status-pill::before {
|
|
1122
|
+
content: "";
|
|
1123
|
+
width: 8px;
|
|
1124
|
+
height: 8px;
|
|
1125
|
+
border-radius: 999px;
|
|
1126
|
+
background: #14b8a6;
|
|
1127
|
+
box-shadow: 0 0 0 4px rgba(20, 184, 166, 0.12);
|
|
1128
|
+
}
|
|
1129
|
+
.status-card h2 {
|
|
1130
|
+
margin: 0;
|
|
1131
|
+
color: #111827;
|
|
1132
|
+
font-size: 20px;
|
|
1133
|
+
line-height: 1.18;
|
|
1134
|
+
}
|
|
1135
|
+
dl {
|
|
1136
|
+
display: grid;
|
|
1137
|
+
grid-template-columns: 118px minmax(0, 1fr);
|
|
1138
|
+
gap: 9px 14px;
|
|
1139
|
+
margin: 0;
|
|
1140
|
+
padding-top: 4px;
|
|
1141
|
+
}
|
|
1142
|
+
dt {
|
|
1143
|
+
color: #64748b;
|
|
1144
|
+
font-size: 12px;
|
|
1145
|
+
font-weight: 650;
|
|
1146
|
+
}
|
|
1147
|
+
dd {
|
|
1148
|
+
min-width: 0;
|
|
1149
|
+
margin: 0;
|
|
1150
|
+
overflow: hidden;
|
|
1151
|
+
color: #111827;
|
|
1152
|
+
font-size: 13px;
|
|
1153
|
+
font-weight: 680;
|
|
1154
|
+
text-overflow: ellipsis;
|
|
1155
|
+
white-space: nowrap;
|
|
1156
|
+
}
|
|
1157
|
+
.actions {
|
|
1158
|
+
display: grid;
|
|
1159
|
+
grid-template-columns: 1fr 1fr;
|
|
1160
|
+
gap: 10px;
|
|
1161
|
+
}
|
|
1162
|
+
button, a.button {
|
|
1163
|
+
min-height: 40px;
|
|
1164
|
+
display: inline-flex;
|
|
1165
|
+
align-items: center;
|
|
1166
|
+
justify-content: center;
|
|
1167
|
+
border: 1px solid #111827;
|
|
1168
|
+
border-radius: 9px;
|
|
1169
|
+
background: #111827;
|
|
1170
|
+
color: #ffffff;
|
|
1171
|
+
font-size: 13px;
|
|
1172
|
+
font-weight: 680;
|
|
1173
|
+
text-decoration: none;
|
|
1174
|
+
cursor: pointer;
|
|
1175
|
+
box-shadow: 0 12px 26px rgba(15, 23, 42, 0.13);
|
|
1176
|
+
}
|
|
1177
|
+
button.secondary, a.secondary {
|
|
1178
|
+
border-color: rgba(148, 163, 184, 0.36);
|
|
1179
|
+
background: #ffffff;
|
|
1180
|
+
color: #1f2937;
|
|
1181
|
+
box-shadow: 0 8px 20px rgba(15, 23, 42, 0.08);
|
|
1182
|
+
}
|
|
1183
|
+
.muted-card {
|
|
1184
|
+
padding: 14px 16px;
|
|
1185
|
+
border: 1px dashed rgba(148, 163, 184, 0.34);
|
|
1186
|
+
border-radius: 12px;
|
|
1187
|
+
background: rgba(248, 250, 252, 0.8);
|
|
1188
|
+
color: #64748b;
|
|
1189
|
+
font-size: 13px;
|
|
1190
|
+
line-height: 1.55;
|
|
1191
|
+
font-weight: 500;
|
|
1192
|
+
}
|
|
1193
|
+
@media (max-width: 760px) {
|
|
1194
|
+
body { padding: 14px; }
|
|
1195
|
+
main { grid-template-columns: 1fr; }
|
|
1196
|
+
.hero { border-right: 0; border-bottom: 1px solid rgba(148, 163, 184, 0.22); }
|
|
1197
|
+
.hero, .panel { padding: 24px; }
|
|
1198
|
+
h1 { font-size: 28px; }
|
|
1199
|
+
.actions { grid-template-columns: 1fr; }
|
|
1200
|
+
}
|
|
1201
|
+
</style>
|
|
1202
|
+
</head>
|
|
1203
|
+
<body>
|
|
1204
|
+
<main>
|
|
1205
|
+
<section class="hero">
|
|
1206
|
+
<div class="brand">
|
|
1207
|
+
<div class="mark">LD</div>
|
|
1208
|
+
<div class="brand-copy">
|
|
1209
|
+
<strong>LiveDesk</strong>
|
|
1210
|
+
<span>Client dashboard</span>
|
|
1211
|
+
</div>
|
|
1212
|
+
</div>
|
|
1213
|
+
<div class="headline">
|
|
1214
|
+
<span class="eyebrow">This computer</span>
|
|
1215
|
+
<h1>Client is ready</h1>
|
|
1216
|
+
<p id="message">${escapeHtml(message)}</p>
|
|
1217
|
+
</div>
|
|
1218
|
+
<p>Keep this page open if you want a local control surface for this workstation. Reboots can reconnect automatically when startup is enabled.</p>
|
|
1219
|
+
</section>
|
|
1220
|
+
<section class="panel">
|
|
1221
|
+
<article class="status-card">
|
|
1222
|
+
<div class="status-head">
|
|
1223
|
+
<h2>Connection</h2>
|
|
1224
|
+
<span id="status" class="status-pill">${escapeHtml(statusLabel)}</span>
|
|
1225
|
+
</div>
|
|
1226
|
+
<dl>
|
|
1227
|
+
<dt>Auth</dt><dd id="auth">${escapeHtml(authLabel)}</dd>
|
|
1228
|
+
<dt>Hub</dt><dd id="manager">${escapeHtml(manager || 'waiting')}</dd>
|
|
1229
|
+
<dt>Placement</dt><dd id="slot">${escapeHtml(slot)}</dd>
|
|
1230
|
+
<dt>Startup</dt><dd id="startup">${state.startup ? 'enabled' : 'manual'}</dd>
|
|
1231
|
+
<dt>Started</dt><dd id="connected-at">${escapeHtml(connectedAt)}</dd>
|
|
1232
|
+
</dl>
|
|
1233
|
+
</article>
|
|
1234
|
+
<div class="actions">
|
|
1235
|
+
<a class="button secondary" href="/">Refresh dashboard</a>
|
|
1236
|
+
<form method="post" action="/logout">
|
|
1237
|
+
<button class="secondary" type="submit">Logout</button>
|
|
1238
|
+
</form>
|
|
1239
|
+
</div>
|
|
1240
|
+
<div class="muted-card">The terminal process starts the screen stream automatically after auth. This dashboard stays available for logout and future workstation controls.</div>
|
|
1241
|
+
</section>
|
|
1242
|
+
</main>
|
|
1243
|
+
<script>
|
|
1244
|
+
const initialState = ${stateJson};
|
|
1245
|
+
function setText(id, value) {
|
|
1246
|
+
const node = document.getElementById(id);
|
|
1247
|
+
if (node) node.textContent = value || '';
|
|
1248
|
+
}
|
|
1249
|
+
function applyState(state) {
|
|
1250
|
+
setText('status', state.statusLabel || initialState.statusLabel);
|
|
1251
|
+
setText('auth', state.authLabel || initialState.authLabel);
|
|
1252
|
+
setText('manager', state.manager || 'waiting');
|
|
1253
|
+
setText('slot', state.slot || initialState.slot);
|
|
1254
|
+
setText('startup', state.startup ? 'enabled' : 'manual');
|
|
1255
|
+
setText('connected-at', state.connectedAt || initialState.connectedAt);
|
|
1256
|
+
setText('message', state.message || initialState.message);
|
|
1257
|
+
}
|
|
1258
|
+
applyState(initialState);
|
|
1259
|
+
if (location.pathname === '/callback') {
|
|
1260
|
+
history.replaceState(null, '', '/');
|
|
1261
|
+
}
|
|
1262
|
+
async function refreshState() {
|
|
1263
|
+
try {
|
|
1264
|
+
const response = await fetch('/api/state', { cache: 'no-store' });
|
|
1265
|
+
if (!response.ok) return;
|
|
1266
|
+
applyState(await response.json());
|
|
1267
|
+
} catch {
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
setInterval(refreshState, 1000);
|
|
1271
|
+
refreshState();
|
|
943
1272
|
</script>
|
|
944
1273
|
</body>
|
|
945
1274
|
</html>`;
|
|
@@ -950,6 +1279,32 @@ function normalizePairingPin(value) {
|
|
|
950
1279
|
return /^\d{6}$/.test(pin) ? pin : '';
|
|
951
1280
|
}
|
|
952
1281
|
|
|
1282
|
+
function readSavedPin() {
|
|
1283
|
+
try {
|
|
1284
|
+
const state = JSON.parse(readFileSync(CLIENT_PIN_PATH, 'utf8'));
|
|
1285
|
+
return normalizePairingPin(state?.pin);
|
|
1286
|
+
} catch {
|
|
1287
|
+
return '';
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
function writeSavedPin(pin) {
|
|
1292
|
+
const normalizedPin = normalizePairingPin(pin);
|
|
1293
|
+
if (!normalizedPin) {
|
|
1294
|
+
return false;
|
|
1295
|
+
}
|
|
1296
|
+
mkdirSync(dirname(CLIENT_PIN_PATH), { recursive: true });
|
|
1297
|
+
writeFileSync(CLIENT_PIN_PATH, JSON.stringify({
|
|
1298
|
+
pin: normalizedPin,
|
|
1299
|
+
updatedAt: new Date().toISOString()
|
|
1300
|
+
}, null, 2));
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function clearSavedPin() {
|
|
1305
|
+
rmSync(CLIENT_PIN_PATH, { force: true });
|
|
1306
|
+
}
|
|
1307
|
+
|
|
953
1308
|
function readRequestBody(req, maxBytes = 4096) {
|
|
954
1309
|
return new Promise((resolve, reject) => {
|
|
955
1310
|
let body = '';
|
|
@@ -1001,16 +1356,57 @@ async function resolveManagerFromPin(supabase, pin) {
|
|
|
1001
1356
|
};
|
|
1002
1357
|
}
|
|
1003
1358
|
|
|
1359
|
+
async function waitForManagerFromSavedPin(supabase, pin, options = {}) {
|
|
1360
|
+
const intervalMs = Math.max(1000, Number(options.intervalMs || DISCOVERY_RETRY_MS));
|
|
1361
|
+
const intervalSeconds = Math.max(1, Math.round(intervalMs / 1000));
|
|
1362
|
+
const shouldStop = typeof options.shouldStop === 'function' ? options.shouldStop : () => false;
|
|
1363
|
+
let attempts = 0;
|
|
1364
|
+
let lastMessage = '';
|
|
1365
|
+
console.log(`Waiting for a LiveDesk Hub from saved PIN. This client will keep trying every ${intervalSeconds}s.`);
|
|
1366
|
+
while (true) {
|
|
1367
|
+
if (shouldStop()) {
|
|
1368
|
+
return null;
|
|
1369
|
+
}
|
|
1370
|
+
attempts += 1;
|
|
1371
|
+
try {
|
|
1372
|
+
return await resolveManagerFromPin(supabase, pin);
|
|
1373
|
+
} catch (err) {
|
|
1374
|
+
if (shouldStop()) {
|
|
1375
|
+
return null;
|
|
1376
|
+
}
|
|
1377
|
+
const message = formatDiscoveryError(err);
|
|
1378
|
+
if (message !== lastMessage || attempts === 1 || attempts % 6 === 0) {
|
|
1379
|
+
const suffix = attempts === 1 ? '' : ` attempt ${attempts}`;
|
|
1380
|
+
console.log(`Still waiting for LiveDesk Hub by saved PIN${suffix}: ${message}`);
|
|
1381
|
+
lastMessage = message;
|
|
1382
|
+
}
|
|
1383
|
+
await sleep(intervalMs);
|
|
1384
|
+
}
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1387
|
+
|
|
1004
1388
|
async function startConnectionChoiceServer(supabase, options = {}) {
|
|
1005
1389
|
const host = String(process.env.LIVEDESK_CLIENT_AUTH_HOST || DEFAULT_AUTH_CALLBACK_HOST).trim() || DEFAULT_AUTH_CALLBACK_HOST;
|
|
1006
1390
|
const port = normalizePort(options.authPort) || DEFAULT_AUTH_CALLBACK_PORT;
|
|
1007
1391
|
const slot = normalizeSlotNumber(options.slot);
|
|
1008
1392
|
const startupArgs = Array.isArray(options.startupArgs) ? options.startupArgs : [];
|
|
1393
|
+
const savedSession = options.savedSession?.access_token ? options.savedSession : null;
|
|
1394
|
+
const savedPin = normalizePairingPin(options.savedPin);
|
|
1009
1395
|
let pendingStartup = isWindowsStartupRegistered();
|
|
1010
1396
|
let listeningPort = port;
|
|
1011
1397
|
let completed = false;
|
|
1012
1398
|
let completedTitle = 'LiveDesk connection complete';
|
|
1013
|
-
let completedMessage = '
|
|
1399
|
+
let completedMessage = 'The LiveDesk client is starting automatically.';
|
|
1400
|
+
const dashboardState = {
|
|
1401
|
+
choice: null,
|
|
1402
|
+
connectedAt: '',
|
|
1403
|
+
endpointCandidates: [],
|
|
1404
|
+
loggedOut: false,
|
|
1405
|
+
manager: '',
|
|
1406
|
+
message: completedMessage,
|
|
1407
|
+
slot,
|
|
1408
|
+
startup: pendingStartup
|
|
1409
|
+
};
|
|
1014
1410
|
let settleChoice;
|
|
1015
1411
|
let rejectChoice;
|
|
1016
1412
|
const waitForChoice = new Promise((resolve, reject) => {
|
|
@@ -1022,8 +1418,18 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1022
1418
|
completed = true;
|
|
1023
1419
|
completedTitle = title;
|
|
1024
1420
|
completedMessage = message;
|
|
1025
|
-
|
|
1026
|
-
|
|
1421
|
+
dashboardState.choice = choice;
|
|
1422
|
+
dashboardState.connectedAt = new Date().toISOString();
|
|
1423
|
+
dashboardState.endpointCandidates = Array.isArray(choice?.endpointCandidates) ? choice.endpointCandidates : [];
|
|
1424
|
+
dashboardState.loggedOut = false;
|
|
1425
|
+
dashboardState.manager = choice?.manager || '';
|
|
1426
|
+
dashboardState.message = message;
|
|
1427
|
+
dashboardState.startup = pendingStartup;
|
|
1428
|
+
if (settleChoice) {
|
|
1429
|
+
const resolveChoice = settleChoice;
|
|
1430
|
+
settleChoice = null;
|
|
1431
|
+
resolveChoice(choice);
|
|
1432
|
+
}
|
|
1027
1433
|
};
|
|
1028
1434
|
const renderChoice = (props = {}) => renderConnectionChoicePage({
|
|
1029
1435
|
slot,
|
|
@@ -1031,6 +1437,63 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1031
1437
|
startupSupported: isWindowsStartupSupported(),
|
|
1032
1438
|
...props
|
|
1033
1439
|
});
|
|
1440
|
+
const renderDashboard = (props = {}) => renderConnectionDashboardPage({
|
|
1441
|
+
...dashboardState,
|
|
1442
|
+
slot,
|
|
1443
|
+
startup: pendingStartup,
|
|
1444
|
+
...props
|
|
1445
|
+
});
|
|
1446
|
+
const getDashboardApiState = () => {
|
|
1447
|
+
const manager = dashboardState.manager || dashboardState.choice?.manager || '';
|
|
1448
|
+
return {
|
|
1449
|
+
authLabel: getDashboardAuthLabel(dashboardState.choice, dashboardState.loggedOut),
|
|
1450
|
+
connectedAt: dashboardState.connectedAt || '',
|
|
1451
|
+
manager,
|
|
1452
|
+
message: dashboardState.message || completedMessage,
|
|
1453
|
+
slot: normalizeSlotNumber(slot) ? `Slot ${String(slot).padStart(3, '0')}` : 'First available',
|
|
1454
|
+
startup: Boolean(pendingStartup),
|
|
1455
|
+
completed,
|
|
1456
|
+
statusLabel: dashboardState.loggedOut
|
|
1457
|
+
? 'Signed out for next restart'
|
|
1458
|
+
: manager
|
|
1459
|
+
? 'Client starting'
|
|
1460
|
+
: completed
|
|
1461
|
+
? 'Finding Hub'
|
|
1462
|
+
: 'Waiting for auth'
|
|
1463
|
+
};
|
|
1464
|
+
};
|
|
1465
|
+
|
|
1466
|
+
const startSavedConnection = async () => {
|
|
1467
|
+
if (completed || dashboardState.loggedOut) {
|
|
1468
|
+
return;
|
|
1469
|
+
}
|
|
1470
|
+
if (savedSession?.access_token) {
|
|
1471
|
+
applyStartupPreference(pendingStartup, startupArgs);
|
|
1472
|
+
complete({ type: 'google', session: savedSession }, 'LiveDesk client dashboard', 'Saved Google sign-in found. The client is starting automatically.');
|
|
1473
|
+
return;
|
|
1474
|
+
}
|
|
1475
|
+
if (!savedPin) {
|
|
1476
|
+
return;
|
|
1477
|
+
}
|
|
1478
|
+
dashboardState.message = 'Saved LiveDesk PIN found. Waiting for the Hub to become reachable.';
|
|
1479
|
+
try {
|
|
1480
|
+
const resolved = await waitForManagerFromSavedPin(supabase, savedPin, {
|
|
1481
|
+
shouldStop: () => completed || dashboardState.loggedOut
|
|
1482
|
+
});
|
|
1483
|
+
if (!resolved) {
|
|
1484
|
+
return;
|
|
1485
|
+
}
|
|
1486
|
+
if (completed || dashboardState.loggedOut) {
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
applyStartupPreference(pendingStartup, startupArgs);
|
|
1490
|
+
complete({ type: 'pin', ...resolved }, 'LiveDesk client dashboard', 'Saved PIN accepted. The client is starting automatically.');
|
|
1491
|
+
} catch (err) {
|
|
1492
|
+
if (!completed) {
|
|
1493
|
+
dashboardState.message = `Saved PIN did not connect: ${formatDiscoveryError(err)}`;
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
};
|
|
1034
1497
|
|
|
1035
1498
|
const handleError = (res, error, pin = '') => {
|
|
1036
1499
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
@@ -1048,15 +1511,45 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1048
1511
|
res.end();
|
|
1049
1512
|
return;
|
|
1050
1513
|
}
|
|
1051
|
-
|
|
1514
|
+
|
|
1515
|
+
if (requestUrl.pathname === '/api/state') {
|
|
1516
|
+
res.writeHead(200, {
|
|
1517
|
+
'Content-Type': 'application/json; charset=utf-8',
|
|
1518
|
+
'Cache-Control': 'no-store'
|
|
1519
|
+
});
|
|
1520
|
+
res.end(JSON.stringify(getDashboardApiState()));
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (requestUrl.pathname === '/logout') {
|
|
1525
|
+
try {
|
|
1526
|
+
await supabase.auth.signOut();
|
|
1527
|
+
} catch {
|
|
1528
|
+
}
|
|
1529
|
+
rmSync(CLIENT_AUTH_PATH, { force: true });
|
|
1530
|
+
clearSavedPin();
|
|
1531
|
+
dashboardState.choice = null;
|
|
1532
|
+
dashboardState.loggedOut = true;
|
|
1533
|
+
dashboardState.manager = '';
|
|
1534
|
+
dashboardState.message = 'Saved Google sign-in was cleared. Restart this client to sign in again.';
|
|
1535
|
+
if (req.method !== 'GET') {
|
|
1536
|
+
res.writeHead(303, { Location: completed ? '/' : '/connect' });
|
|
1537
|
+
res.end();
|
|
1538
|
+
return;
|
|
1539
|
+
}
|
|
1052
1540
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1053
|
-
res.end(
|
|
1054
|
-
|
|
1055
|
-
message: completedMessage
|
|
1541
|
+
res.end(completed ? renderDashboard() : renderChoice({
|
|
1542
|
+
error: 'Saved Google sign-in was cleared.'
|
|
1056
1543
|
}));
|
|
1057
1544
|
return;
|
|
1058
1545
|
}
|
|
1059
1546
|
|
|
1547
|
+
if (completed) {
|
|
1548
|
+
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1549
|
+
res.end(renderDashboard());
|
|
1550
|
+
return;
|
|
1551
|
+
}
|
|
1552
|
+
|
|
1060
1553
|
if (requestUrl.pathname === '/' || requestUrl.pathname === '/connect') {
|
|
1061
1554
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1062
1555
|
res.end(renderChoice());
|
|
@@ -1068,12 +1561,10 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1068
1561
|
const { data: existing } = await supabase.auth.getSession();
|
|
1069
1562
|
if (existing?.session?.access_token) {
|
|
1070
1563
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
1564
|
+
const choice = { type: 'google', session: existing.session };
|
|
1565
|
+
complete(choice, 'LiveDesk client dashboard', 'Signed in. The client is starting automatically.');
|
|
1071
1566
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1072
|
-
res.end(
|
|
1073
|
-
title: 'LiveDesk sign-in ready',
|
|
1074
|
-
message: 'You can close this tab and return to the terminal.'
|
|
1075
|
-
}));
|
|
1076
|
-
complete({ type: 'google', session: existing.session }, 'LiveDesk sign-in ready');
|
|
1567
|
+
res.end(renderDashboard());
|
|
1077
1568
|
return;
|
|
1078
1569
|
}
|
|
1079
1570
|
|
|
@@ -1110,13 +1601,12 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1110
1601
|
pin = normalizePairingPin(pin);
|
|
1111
1602
|
try {
|
|
1112
1603
|
const resolved = await resolveManagerFromPin(supabase, pin);
|
|
1604
|
+
writeSavedPin(pin);
|
|
1113
1605
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
1606
|
+
const choice = { type: 'pin', ...resolved };
|
|
1607
|
+
complete(choice, 'LiveDesk client dashboard', 'PIN accepted. The client is starting automatically.');
|
|
1114
1608
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1115
|
-
res.end(
|
|
1116
|
-
title: 'LiveDesk PIN accepted',
|
|
1117
|
-
message: 'You can close this tab and return to the terminal.'
|
|
1118
|
-
}));
|
|
1119
|
-
complete({ type: 'pin', ...resolved }, 'LiveDesk PIN accepted');
|
|
1609
|
+
res.end(renderDashboard());
|
|
1120
1610
|
} catch (err) {
|
|
1121
1611
|
handleError(res, err, pin);
|
|
1122
1612
|
}
|
|
@@ -1134,7 +1624,9 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1134
1624
|
tone: 'error'
|
|
1135
1625
|
}));
|
|
1136
1626
|
completed = true;
|
|
1137
|
-
|
|
1627
|
+
if (rejectChoice) {
|
|
1628
|
+
rejectChoice(new Error(error));
|
|
1629
|
+
}
|
|
1138
1630
|
server.close();
|
|
1139
1631
|
return;
|
|
1140
1632
|
}
|
|
@@ -1155,17 +1647,17 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1155
1647
|
tone: 'error'
|
|
1156
1648
|
}));
|
|
1157
1649
|
completed = true;
|
|
1158
|
-
|
|
1650
|
+
if (rejectChoice) {
|
|
1651
|
+
rejectChoice(message instanceof Error ? message : new Error(String(message)));
|
|
1652
|
+
}
|
|
1159
1653
|
server.close();
|
|
1160
1654
|
return;
|
|
1161
1655
|
}
|
|
1162
1656
|
applyStartupPreference(pendingStartup, startupArgs);
|
|
1657
|
+
const choice = { type: 'google', session: sessionData.session };
|
|
1658
|
+
complete(choice, 'LiveDesk client dashboard', 'Signed in. The client is starting automatically.');
|
|
1163
1659
|
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
|
|
1164
|
-
res.end(
|
|
1165
|
-
title: 'LiveDesk sign-in complete',
|
|
1166
|
-
message: 'You can close this tab and return to the terminal.'
|
|
1167
|
-
}));
|
|
1168
|
-
complete({ type: 'google', session: sessionData.session }, 'LiveDesk sign-in complete');
|
|
1660
|
+
res.end(renderDashboard());
|
|
1169
1661
|
return;
|
|
1170
1662
|
}
|
|
1171
1663
|
|
|
@@ -1184,7 +1676,9 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1184
1676
|
}));
|
|
1185
1677
|
if (!completed) {
|
|
1186
1678
|
completed = true;
|
|
1187
|
-
|
|
1679
|
+
if (rejectChoice) {
|
|
1680
|
+
rejectChoice(err instanceof Error ? err : new Error(String(err)));
|
|
1681
|
+
}
|
|
1188
1682
|
server.close();
|
|
1189
1683
|
}
|
|
1190
1684
|
});
|
|
@@ -1196,6 +1690,11 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1196
1690
|
});
|
|
1197
1691
|
const address = server.address();
|
|
1198
1692
|
listeningPort = typeof address === 'object' && address ? address.port : port;
|
|
1693
|
+
if (savedSession?.access_token || savedPin) {
|
|
1694
|
+
setImmediate(() => {
|
|
1695
|
+
void startSavedConnection();
|
|
1696
|
+
});
|
|
1697
|
+
}
|
|
1199
1698
|
} catch (err) {
|
|
1200
1699
|
if (err?.code === 'EADDRINUSE') {
|
|
1201
1700
|
throw new Error(`LiveDesk connection page port ${port} is already in use. Close the app using it or run with --auth-port <port> and add that callback URL in Supabase Auth redirect URLs.`);
|
|
@@ -1206,7 +1705,16 @@ async function startConnectionChoiceServer(supabase, options = {}) {
|
|
|
1206
1705
|
get url() {
|
|
1207
1706
|
return `http://${host}:${listeningPort}/`;
|
|
1208
1707
|
},
|
|
1209
|
-
waitForChoice
|
|
1708
|
+
waitForChoice,
|
|
1709
|
+
close() {
|
|
1710
|
+
try {
|
|
1711
|
+
server.close();
|
|
1712
|
+
} catch {
|
|
1713
|
+
}
|
|
1714
|
+
},
|
|
1715
|
+
update(patch = {}) {
|
|
1716
|
+
Object.assign(dashboardState, patch);
|
|
1717
|
+
}
|
|
1210
1718
|
};
|
|
1211
1719
|
}
|
|
1212
1720
|
|
|
@@ -1218,7 +1726,8 @@ async function chooseClientConnection(supabase, options = {}) {
|
|
|
1218
1726
|
});
|
|
1219
1727
|
console.log('Opening LiveDesk connection page...');
|
|
1220
1728
|
openBrowser(connectionPage.url);
|
|
1221
|
-
|
|
1729
|
+
const choice = await connectionPage.waitForChoice;
|
|
1730
|
+
return { ...choice, connectionPage };
|
|
1222
1731
|
}
|
|
1223
1732
|
|
|
1224
1733
|
function parseManagerEndpoint(value) {
|
|
@@ -1342,28 +1851,43 @@ async function prepareLoginConnection(parsed) {
|
|
|
1342
1851
|
let forwarded = [...parsed.forwarded];
|
|
1343
1852
|
let manager = parsed.manager;
|
|
1344
1853
|
let pair = parsed.pair;
|
|
1854
|
+
let connectionPage = null;
|
|
1345
1855
|
|
|
1346
1856
|
if (shouldLogin) {
|
|
1347
1857
|
const supabase = await createSupabaseClient();
|
|
1348
1858
|
const startupArgs = buildStartupClientArgs(parsed);
|
|
1349
1859
|
let savedSession = null;
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1860
|
+
try {
|
|
1861
|
+
savedSession = await refreshSessionIfNeeded(supabase);
|
|
1862
|
+
} catch (err) {
|
|
1863
|
+
console.warn(`LiveDesk saved sign-in could not be refreshed: ${err?.message || err}`);
|
|
1864
|
+
}
|
|
1865
|
+
const savedPin = readSavedPin();
|
|
1866
|
+
let choice = null;
|
|
1867
|
+
if (parsed.startupRun && savedSession?.access_token) {
|
|
1868
|
+
choice = { type: 'google', session: savedSession };
|
|
1869
|
+
} else if (parsed.startupRun && savedPin) {
|
|
1870
|
+
const resolved = await waitForManagerFromSavedPin(supabase, savedPin);
|
|
1871
|
+
choice = { type: 'pin', ...resolved };
|
|
1356
1872
|
}
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
: await chooseClientConnection(supabase, {
|
|
1873
|
+
if (!choice) {
|
|
1874
|
+
choice = await chooseClientConnection(supabase, {
|
|
1360
1875
|
authPort: parsed.authPort,
|
|
1361
1876
|
slot: parsed.slot,
|
|
1362
|
-
startupArgs
|
|
1877
|
+
startupArgs,
|
|
1878
|
+
savedSession,
|
|
1879
|
+
savedPin
|
|
1363
1880
|
});
|
|
1881
|
+
}
|
|
1882
|
+
connectionPage = choice.connectionPage || null;
|
|
1364
1883
|
if (choice.type === 'pin') {
|
|
1365
1884
|
manager = choice.manager;
|
|
1366
1885
|
pair = choice.pair;
|
|
1886
|
+
connectionPage?.update({
|
|
1887
|
+
endpointCandidates: choice.endpointCandidates || [],
|
|
1888
|
+
manager,
|
|
1889
|
+
message: `Connected to LiveDesk Hub at ${manager}.`
|
|
1890
|
+
});
|
|
1367
1891
|
console.log('Connected by LiveDesk PIN.');
|
|
1368
1892
|
} else {
|
|
1369
1893
|
const session = choice.session;
|
|
@@ -1372,6 +1896,11 @@ async function prepareLoginConnection(parsed) {
|
|
|
1372
1896
|
const resolved = await waitForManagerFromSupabase(supabase);
|
|
1373
1897
|
manager = resolved.manager;
|
|
1374
1898
|
pair = resolved.pair;
|
|
1899
|
+
connectionPage?.update({
|
|
1900
|
+
endpointCandidates: resolved.endpointCandidates || [],
|
|
1901
|
+
manager,
|
|
1902
|
+
message: `Connected to LiveDesk Hub at ${manager}.`
|
|
1903
|
+
});
|
|
1375
1904
|
}
|
|
1376
1905
|
console.log(`Found LiveDesk Hub at ${manager}.`);
|
|
1377
1906
|
}
|
|
@@ -1394,6 +1923,7 @@ async function prepareLoginConnection(parsed) {
|
|
|
1394
1923
|
manager,
|
|
1395
1924
|
pair,
|
|
1396
1925
|
slot,
|
|
1926
|
+
connectionPage,
|
|
1397
1927
|
forwarded,
|
|
1398
1928
|
rediscoverOnInvalidPair: shouldLogin
|
|
1399
1929
|
};
|
|
@@ -1674,6 +2204,7 @@ async function main() {
|
|
|
1674
2204
|
result = await spawnAgent(fastLaunch.command, [...fastLaunch.argsPrefix, ...fastArgs], buildFastEnvironment());
|
|
1675
2205
|
} else if (prepared.engine === 'fast') {
|
|
1676
2206
|
console.error(`C# RemoteFast is unavailable: ${fastLaunch.reason}. Use --engine node to run the legacy Node agent.`);
|
|
2207
|
+
prepared.connectionPage?.close?.();
|
|
1677
2208
|
process.exit(2);
|
|
1678
2209
|
} else {
|
|
1679
2210
|
console.warn(`C# RemoteFast is unavailable (${fastLaunch.reason}). Falling back to the Node remote agent.`);
|
|
@@ -1682,6 +2213,7 @@ async function main() {
|
|
|
1682
2213
|
} else {
|
|
1683
2214
|
result = await spawnAgent(process.execPath, [nodeAgentPath, ...prepared.forwarded]);
|
|
1684
2215
|
}
|
|
2216
|
+
prepared.connectionPage?.close?.();
|
|
1685
2217
|
if (result?.signal) {
|
|
1686
2218
|
process.kill(process.pid, result.signal);
|
|
1687
2219
|
return;
|