@woniru/we-installer 0.1.0
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 +107 -0
- package/bin/we-installer.js +34 -0
- package/package.json +20 -0
- package/src/auth.js +87 -0
- package/src/authServer.js +490 -0
- package/src/download.js +123 -0
- package/src/extractZip.js +32 -0
- package/src/githubApi.js +22 -0
- package/src/githubDeviceFlow.js +82 -0
- package/src/githubDownload.js +33 -0
- package/src/prompt.js +14 -0
- package/src/sqlRunner.js +165 -0
- package/templates/auth.html +377 -0
- package/templates/configure.html +488 -0
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>WE Installer • Configure</title>
|
|
8
|
+
<style>
|
|
9
|
+
body {
|
|
10
|
+
font-family: system-ui;
|
|
11
|
+
margin: 24px;
|
|
12
|
+
line-height: 1.4;
|
|
13
|
+
background-color: #F6F8FA;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
.card {
|
|
17
|
+
background-color: white;
|
|
18
|
+
width: min(760px, calc(100vw - 48px));
|
|
19
|
+
border: 1px solid #ddd;
|
|
20
|
+
border-radius: 12px;
|
|
21
|
+
padding: 20px;
|
|
22
|
+
margin: 0 auto;
|
|
23
|
+
box-shadow: 0px 1px 1px 0px #1f23280f, 0px 1px 3px 0px #1f23280f;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
h2 {
|
|
27
|
+
margin: 0 0 8px 0;
|
|
28
|
+
font-size: 22px;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
h3 {
|
|
32
|
+
margin: 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.muted {
|
|
36
|
+
color: #666;
|
|
37
|
+
margin-top: 8px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.steps {
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: 10px;
|
|
43
|
+
margin: 12px 0 18px;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.step {
|
|
48
|
+
border: 1px solid #e5e7eb;
|
|
49
|
+
background: #fafafa;
|
|
50
|
+
border-radius: 999px;
|
|
51
|
+
padding: 6px 10px;
|
|
52
|
+
font-size: 13px;
|
|
53
|
+
color: #333;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.step.active {
|
|
57
|
+
background: #111;
|
|
58
|
+
color: #fff;
|
|
59
|
+
border-color: #111;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
label {
|
|
63
|
+
display: block;
|
|
64
|
+
margin-top: 14px;
|
|
65
|
+
font-weight: 600;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
input[type="text"],
|
|
69
|
+
input[type="password"] {
|
|
70
|
+
width: 100%;
|
|
71
|
+
box-sizing: border-box;
|
|
72
|
+
margin-top: 8px;
|
|
73
|
+
padding: 10px 12px;
|
|
74
|
+
border-radius: 10px;
|
|
75
|
+
border: 1px solid #cfd6dd;
|
|
76
|
+
font-size: 14px;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
.row {
|
|
80
|
+
display: flex;
|
|
81
|
+
gap: 10px;
|
|
82
|
+
align-items: center;
|
|
83
|
+
flex-wrap: wrap;
|
|
84
|
+
margin-top: 12px;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
.checkboxRow {
|
|
88
|
+
margin-top: 12px;
|
|
89
|
+
display: flex;
|
|
90
|
+
gap: 10px;
|
|
91
|
+
align-items: center;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
button {
|
|
95
|
+
display: inline-block;
|
|
96
|
+
padding: 10px 14px;
|
|
97
|
+
border-radius: 10px;
|
|
98
|
+
border: 1px solid #333;
|
|
99
|
+
background: #111;
|
|
100
|
+
color: #fff;
|
|
101
|
+
cursor: pointer;
|
|
102
|
+
font-size: 16px;
|
|
103
|
+
line-height: 1.4;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
button.secondary {
|
|
107
|
+
background: #fff;
|
|
108
|
+
color: #111;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
button:disabled {
|
|
112
|
+
opacity: 0.6;
|
|
113
|
+
cursor: not-allowed;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
.statusBox {
|
|
117
|
+
margin-top: 18px;
|
|
118
|
+
padding: 10px 12px;
|
|
119
|
+
border-radius: 10px;
|
|
120
|
+
background: #f6f6f6;
|
|
121
|
+
border: 1px solid #eee;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
.error {
|
|
125
|
+
color: #b42318;
|
|
126
|
+
margin-top: 10px;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.ok {
|
|
130
|
+
color: #067647;
|
|
131
|
+
margin-top: 10px;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.path,
|
|
135
|
+
.codebox {
|
|
136
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
|
|
137
|
+
font-size: 13px;
|
|
138
|
+
background: #f8fafc;
|
|
139
|
+
border: 1px solid #e5e7eb;
|
|
140
|
+
border-radius: 10px;
|
|
141
|
+
padding: 10px 12px;
|
|
142
|
+
margin-top: 10px;
|
|
143
|
+
white-space: pre-wrap;
|
|
144
|
+
word-break: break-word;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.section {
|
|
148
|
+
margin-top: 16px;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.pill {
|
|
152
|
+
display: inline-block;
|
|
153
|
+
padding: 4px 10px;
|
|
154
|
+
border-radius: 999px;
|
|
155
|
+
border: 1px solid #e5e7eb;
|
|
156
|
+
background: #fafafa;
|
|
157
|
+
font-size: 12px;
|
|
158
|
+
margin-top: 8px;
|
|
159
|
+
}
|
|
160
|
+
</style>
|
|
161
|
+
</head>
|
|
162
|
+
|
|
163
|
+
<body>
|
|
164
|
+
<div class="card">
|
|
165
|
+
<h2 id="pageTitle">Configure WoniruEngine</h2>
|
|
166
|
+
<div class="muted" id="stepMeta">Step 1 of 3</div>
|
|
167
|
+
|
|
168
|
+
<div class="steps">
|
|
169
|
+
<div class="step" id="s1">1) Setup Redis</div>
|
|
170
|
+
<div class="step" id="s2">2) Connect Database</div>
|
|
171
|
+
<div class="step" id="s3">3) Admin User</div>
|
|
172
|
+
</div>
|
|
173
|
+
|
|
174
|
+
<div class="statusBox" id="globalStatus">Status: Loading…</div>
|
|
175
|
+
|
|
176
|
+
<div class="section">
|
|
177
|
+
<div class="muted">Project directory</div>
|
|
178
|
+
<div class="path" id="installDir">Loading…</div>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- STEP 1 -->
|
|
182
|
+
<div id="step1" class="section">
|
|
183
|
+
<h3>Step 1: Setup Redis</h3>
|
|
184
|
+
<div class="muted">
|
|
185
|
+
Redis is expected at <b>127.0.0.1:6379</b>.
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div class="checkboxRow">
|
|
189
|
+
<input id="existingRedis" type="checkbox" />
|
|
190
|
+
<label for="existingRedis" style="margin:0; font-weight:600;">I already have Redis
|
|
191
|
+
installed/configured</label>
|
|
192
|
+
</div>
|
|
193
|
+
|
|
194
|
+
<label for="redisAdminKey">Redis admin key</label>
|
|
195
|
+
<input id="redisAdminKey" type="password" placeholder="Enter admin key (min 8 chars)"
|
|
196
|
+
autocomplete="new-password" />
|
|
197
|
+
<div class="muted">
|
|
198
|
+
If you checked “already configured”, we’ll only <b>test connection</b> as <b>admin</b>.
|
|
199
|
+
Otherwise, we’ll <b>build & start Redis via Docker</b> and configure ACL users.
|
|
200
|
+
</div>
|
|
201
|
+
|
|
202
|
+
<div class="row">
|
|
203
|
+
<button id="saveRedisBtn" type="button">Save & Continue</button>
|
|
204
|
+
<button id="reloadBtn" class="secondary" type="button">Reload</button>
|
|
205
|
+
</div>
|
|
206
|
+
|
|
207
|
+
<div id="msg1"></div>
|
|
208
|
+
</div>
|
|
209
|
+
|
|
210
|
+
<!-- STEP 2 -->
|
|
211
|
+
<div id="step2" class="section" style="display:none;">
|
|
212
|
+
<h3>Step 2: Connect Database</h3>
|
|
213
|
+
<div class="muted">Enter database connection details. The installer will apply the schema.</div>
|
|
214
|
+
|
|
215
|
+
<label for="dbHost">DB Host</label>
|
|
216
|
+
<input id="dbHost" type="text" placeholder="e.g. 127.0.0.1 or db.mycompany.local" />
|
|
217
|
+
|
|
218
|
+
<label for="dbUser">DB User</label>
|
|
219
|
+
<input id="dbUser" type="text" placeholder="e.g. root / we_user" />
|
|
220
|
+
|
|
221
|
+
<label for="dbPassword">DB Password</label>
|
|
222
|
+
<input id="dbPassword" type="password" placeholder="Password" autocomplete="new-password" />
|
|
223
|
+
|
|
224
|
+
<label for="dbName">DB Name</label>
|
|
225
|
+
<input id="dbName" type="text" placeholder="e.g. woniru_engine" />
|
|
226
|
+
|
|
227
|
+
<div class="row">
|
|
228
|
+
<button id="applyDbBtn" type="button">Save & Continue</button>
|
|
229
|
+
<button id="dbReloadBtn" class="secondary" type="button">Reload</button>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<div id="msg2"></div>
|
|
233
|
+
</div>
|
|
234
|
+
|
|
235
|
+
<!-- STEP 3 -->
|
|
236
|
+
<div id="step3" class="section" style="display:none;">
|
|
237
|
+
<h3>Step 3: Admin User</h3>
|
|
238
|
+
<div class="muted">
|
|
239
|
+
WE is ready. Use the following credentials to login for the first time.
|
|
240
|
+
You can change them after logging in.
|
|
241
|
+
</div>
|
|
242
|
+
|
|
243
|
+
<div class="pill"><b>Username:</b> admin@we.com</div>
|
|
244
|
+
<div class="pill" style="margin-left:8px;"><b>Password:</b> weAdmin</div>
|
|
245
|
+
|
|
246
|
+
<div class="section">
|
|
247
|
+
<div class="muted">Run these commands in the terminal (from the project directory):</div>
|
|
248
|
+
|
|
249
|
+
<div class="codebox" id="cmd1">npm run dev --redisAdminKey=...</div>
|
|
250
|
+
<div class="codebox" id="cmd2">npm run recalculate:user-permissions</div>
|
|
251
|
+
|
|
252
|
+
<div class="muted">
|
|
253
|
+
1) Starts the server. <br />
|
|
254
|
+
2) Recalculates all user permissions if required.
|
|
255
|
+
</div>
|
|
256
|
+
</div>
|
|
257
|
+
|
|
258
|
+
<div class="row">
|
|
259
|
+
<button id="finishBtn" type="button">Finish</button>
|
|
260
|
+
<button id="copyCmdsBtn" class="secondary" type="button">Copy commands</button>
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<div id="msg3"></div>
|
|
264
|
+
<div class="muted">This window will close when you click Finish.</div>
|
|
265
|
+
</div>
|
|
266
|
+
</div>
|
|
267
|
+
|
|
268
|
+
<script>
|
|
269
|
+
const globalStatusEl = document.getElementById("globalStatus");
|
|
270
|
+
const installDirEl = document.getElementById("installDir");
|
|
271
|
+
const stepMetaEl = document.getElementById("stepMeta");
|
|
272
|
+
const pageTitleEl = document.getElementById("pageTitle");
|
|
273
|
+
|
|
274
|
+
const step1El = document.getElementById("step1");
|
|
275
|
+
const step2El = document.getElementById("step2");
|
|
276
|
+
const step3El = document.getElementById("step3");
|
|
277
|
+
|
|
278
|
+
const s1 = document.getElementById("s1");
|
|
279
|
+
const s2 = document.getElementById("s2");
|
|
280
|
+
const s3 = document.getElementById("s3");
|
|
281
|
+
|
|
282
|
+
// Step 1
|
|
283
|
+
const existingRedisEl = document.getElementById("existingRedis");
|
|
284
|
+
const adminKeyEl = document.getElementById("redisAdminKey");
|
|
285
|
+
const saveRedisBtn = document.getElementById("saveRedisBtn");
|
|
286
|
+
const reloadBtn = document.getElementById("reloadBtn");
|
|
287
|
+
const msg1El = document.getElementById("msg1");
|
|
288
|
+
|
|
289
|
+
// Step 2
|
|
290
|
+
const dbHostEl = document.getElementById("dbHost");
|
|
291
|
+
const dbUserEl = document.getElementById("dbUser");
|
|
292
|
+
const dbPasswordEl = document.getElementById("dbPassword");
|
|
293
|
+
const dbNameEl = document.getElementById("dbName");
|
|
294
|
+
const applyDbBtn = document.getElementById("applyDbBtn");
|
|
295
|
+
const dbReloadBtn = document.getElementById("dbReloadBtn");
|
|
296
|
+
const msg2El = document.getElementById("msg2");
|
|
297
|
+
|
|
298
|
+
// Step 3
|
|
299
|
+
const cmd1El = document.getElementById("cmd1");
|
|
300
|
+
const cmd2El = document.getElementById("cmd2");
|
|
301
|
+
const finishBtn = document.getElementById("finishBtn");
|
|
302
|
+
const copyCmdsBtn = document.getElementById("copyCmdsBtn");
|
|
303
|
+
const msg3El = document.getElementById("msg3");
|
|
304
|
+
|
|
305
|
+
function setMsg(el, type, text) {
|
|
306
|
+
if (!el) return;
|
|
307
|
+
el.className = type;
|
|
308
|
+
el.innerText = text || "";
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
async function safeJson(url, opts) {
|
|
312
|
+
try {
|
|
313
|
+
const r = await fetch(url, Object.assign({ cache: "no-store" }, opts || {}));
|
|
314
|
+
const j = await r.json();
|
|
315
|
+
return { ok: r.ok, json: j };
|
|
316
|
+
} catch (e) {
|
|
317
|
+
return { ok: false, json: { error: "Network error" } };
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function renderStep(step) {
|
|
322
|
+
s1.className = "step" + (step === 1 ? " active" : "");
|
|
323
|
+
s2.className = "step" + (step === 2 ? " active" : "");
|
|
324
|
+
s3.className = "step" + (step === 3 ? " active" : "");
|
|
325
|
+
|
|
326
|
+
stepMetaEl.innerText = `Step ${step} of 3`;
|
|
327
|
+
|
|
328
|
+
step1El.style.display = (step === 1) ? "" : "none";
|
|
329
|
+
step2El.style.display = (step === 2) ? "" : "none";
|
|
330
|
+
step3El.style.display = (step === 3) ? "" : "none";
|
|
331
|
+
|
|
332
|
+
if (step === 1) pageTitleEl.innerText = "Configure WoniruEngine";
|
|
333
|
+
if (step === 2) pageTitleEl.innerText = "Configure • Database";
|
|
334
|
+
if (step === 3) pageTitleEl.innerText = "Finish • Admin User";
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async function pollGlobalStatus() {
|
|
338
|
+
const res = await safeJson("/status");
|
|
339
|
+
if (res.ok) globalStatusEl.innerText = "Status: " + (res.json.status || "…");
|
|
340
|
+
setTimeout(pollGlobalStatus, 1200);
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function refreshCommands(redisAdminKey) {
|
|
344
|
+
const safeKey = redisAdminKey || "";
|
|
345
|
+
cmd1El.innerText = `npm run dev --redisAdminKey=${safeKey}`;
|
|
346
|
+
cmd2El.innerText = `npm run recalculate:user-permissions`;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function loadState() {
|
|
350
|
+
setMsg(msg1El, "", "");
|
|
351
|
+
setMsg(msg2El, "", "");
|
|
352
|
+
setMsg(msg3El, "", "");
|
|
353
|
+
|
|
354
|
+
const res = await safeJson("/configure/state");
|
|
355
|
+
if (!res.ok) return;
|
|
356
|
+
|
|
357
|
+
const st = res.json || {};
|
|
358
|
+
const step = st.step || 1;
|
|
359
|
+
const redis = st.redis || {};
|
|
360
|
+
const db = st.db || {};
|
|
361
|
+
|
|
362
|
+
installDirEl.innerText = st.installDir || "(unknown)";
|
|
363
|
+
|
|
364
|
+
// Step 1 fields
|
|
365
|
+
existingRedisEl.checked = !!redis.existing;
|
|
366
|
+
adminKeyEl.value = redis.adminKey || "";
|
|
367
|
+
|
|
368
|
+
// Step 2 fields
|
|
369
|
+
if (dbHostEl) dbHostEl.value = db.host || "";
|
|
370
|
+
if (dbUserEl) dbUserEl.value = db.user || "";
|
|
371
|
+
if (dbPasswordEl) dbPasswordEl.value = db.password || "";
|
|
372
|
+
if (dbNameEl) dbNameEl.value = db.name || "";
|
|
373
|
+
|
|
374
|
+
// Step 3 commands
|
|
375
|
+
refreshCommands(redis.adminKey || "");
|
|
376
|
+
|
|
377
|
+
renderStep(step);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
async function applyRedisStep() {
|
|
381
|
+
setMsg(msg1El, "", "");
|
|
382
|
+
saveRedisBtn.disabled = true;
|
|
383
|
+
|
|
384
|
+
const payload = {
|
|
385
|
+
existing: !!existingRedisEl.checked,
|
|
386
|
+
adminKey: adminKeyEl.value || ""
|
|
387
|
+
};
|
|
388
|
+
|
|
389
|
+
const res = await safeJson("/configure/redis/apply", {
|
|
390
|
+
method: "POST",
|
|
391
|
+
headers: { "Content-Type": "application/json" },
|
|
392
|
+
body: JSON.stringify(payload)
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
if (res.ok && res.json && res.json.ok) {
|
|
396
|
+
setMsg(msg1El, "ok", "Redis step completed ✅ Moving to Step 2…");
|
|
397
|
+
await loadState();
|
|
398
|
+
} else {
|
|
399
|
+
setMsg(msg1El, "error", (res.json && res.json.error) ? res.json.error : "Redis step failed.");
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
saveRedisBtn.disabled = false;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
async function applyDbStep() {
|
|
406
|
+
setMsg(msg2El, "", "");
|
|
407
|
+
applyDbBtn.disabled = true;
|
|
408
|
+
|
|
409
|
+
const payload = {
|
|
410
|
+
host: dbHostEl.value || "",
|
|
411
|
+
user: dbUserEl.value || "",
|
|
412
|
+
password: dbPasswordEl.value || "",
|
|
413
|
+
name: dbNameEl.value || ""
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
const res = await safeJson("/configure/db/apply", {
|
|
417
|
+
method: "POST",
|
|
418
|
+
headers: { "Content-Type": "application/json" },
|
|
419
|
+
body: JSON.stringify(payload)
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
if (res.ok && res.json && res.json.ok) {
|
|
423
|
+
setMsg(msg2El, "ok", "Database step completed ✅ Moving to Step 3…");
|
|
424
|
+
await loadState();
|
|
425
|
+
} else {
|
|
426
|
+
setMsg(msg2El, "error", (res.json && res.json.error) ? res.json.error : "Database step failed.");
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
applyDbBtn.disabled = false;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function notifyDone() {
|
|
433
|
+
// Reliable even during tab close
|
|
434
|
+
/* try {
|
|
435
|
+
const blob = new Blob([JSON.stringify({ ok: true })], { type: "application/json" });
|
|
436
|
+
navigator.sendBeacon("/configure/done", blob);
|
|
437
|
+
} catch {
|
|
438
|
+
// best effort fallback
|
|
439
|
+
fetch("/configure/done", { method: "POST" }).catch(() => { });
|
|
440
|
+
} */
|
|
441
|
+
|
|
442
|
+
fetch("/configure/done", { method: "POST" }).catch(() => { });
|
|
443
|
+
close();
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function finish() {
|
|
447
|
+
setMsg(msg3El, "", "");
|
|
448
|
+
notifyDone();
|
|
449
|
+
|
|
450
|
+
setMsg(msg3El, "ok", "Closing… ✅");
|
|
451
|
+
|
|
452
|
+
// Try to close the tab. (Works if opened by script)
|
|
453
|
+
setTimeout(() => {
|
|
454
|
+
window.close();
|
|
455
|
+
}, 150);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
async function copyCommands() {
|
|
459
|
+
try {
|
|
460
|
+
const text = cmd1El.innerText + "\n" + cmd2El.innerText;
|
|
461
|
+
await navigator.clipboard.writeText(text);
|
|
462
|
+
setMsg(msg3El, "ok", "Commands copied ✅");
|
|
463
|
+
} catch {
|
|
464
|
+
setMsg(msg3El, "error", "Clipboard blocked. Copy manually.");
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
// Hook buttons
|
|
469
|
+
saveRedisBtn.addEventListener("click", applyRedisStep);
|
|
470
|
+
reloadBtn.addEventListener("click", loadState);
|
|
471
|
+
|
|
472
|
+
if (applyDbBtn) applyDbBtn.addEventListener("click", applyDbStep);
|
|
473
|
+
if (dbReloadBtn) dbReloadBtn.addEventListener("click", loadState);
|
|
474
|
+
|
|
475
|
+
if (finishBtn) finishBtn.addEventListener("click", finish);
|
|
476
|
+
if (copyCmdsBtn) copyCmdsBtn.addEventListener("click", copyCommands);
|
|
477
|
+
|
|
478
|
+
// If user closes tab manually, still signal the installer to exit
|
|
479
|
+
window.addEventListener("beforeunload", () => {
|
|
480
|
+
notifyDone();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
pollGlobalStatus();
|
|
484
|
+
loadState();
|
|
485
|
+
</script>
|
|
486
|
+
</body>
|
|
487
|
+
|
|
488
|
+
</html>
|