rms-devremote 3.0.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 +154 -0
- package/dist/commands/attach.d.ts +2 -0
- package/dist/commands/attach.js +10 -0
- package/dist/commands/check.d.ts +2 -0
- package/dist/commands/check.js +210 -0
- package/dist/commands/clean.d.ts +2 -0
- package/dist/commands/clean.js +177 -0
- package/dist/commands/dashboard.d.ts +2 -0
- package/dist/commands/dashboard.js +57 -0
- package/dist/commands/link.d.ts +2 -0
- package/dist/commands/link.js +112 -0
- package/dist/commands/ping.d.ts +2 -0
- package/dist/commands/ping.js +21 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +54 -0
- package/dist/commands/status.d.ts +2 -0
- package/dist/commands/status.js +65 -0
- package/dist/commands/unlink.d.ts +2 -0
- package/dist/commands/unlink.js +53 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +55 -0
- package/dist/server/auth.d.ts +6 -0
- package/dist/server/auth.js +32 -0
- package/dist/server/frontend.d.ts +4 -0
- package/dist/server/frontend.js +886 -0
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +283 -0
- package/dist/server/terminal.d.ts +14 -0
- package/dist/server/terminal.js +43 -0
- package/dist/services/battery-worker.d.ts +1 -0
- package/dist/services/battery-worker.js +2 -0
- package/dist/services/battery.d.ts +27 -0
- package/dist/services/battery.js +152 -0
- package/dist/services/config.d.ts +63 -0
- package/dist/services/config.js +84 -0
- package/dist/services/docker.d.ts +25 -0
- package/dist/services/docker.js +75 -0
- package/dist/services/hooks.d.ts +15 -0
- package/dist/services/hooks.js +111 -0
- package/dist/services/ntfy.d.ts +19 -0
- package/dist/services/ntfy.js +63 -0
- package/dist/services/process.d.ts +30 -0
- package/dist/services/process.js +90 -0
- package/dist/services/proxy-worker.d.ts +1 -0
- package/dist/services/proxy-worker.js +12 -0
- package/dist/services/proxy.d.ts +4 -0
- package/dist/services/proxy.js +195 -0
- package/dist/services/shell.d.ts +22 -0
- package/dist/services/shell.js +47 -0
- package/dist/services/tmux.d.ts +30 -0
- package/dist/services/tmux.js +74 -0
- package/dist/services/ttyd.d.ts +28 -0
- package/dist/services/ttyd.js +71 -0
- package/dist/setup-server/routes.d.ts +4 -0
- package/dist/setup-server/routes.js +177 -0
- package/dist/setup-server/server.d.ts +4 -0
- package/dist/setup-server/server.js +32 -0
- package/docker/docker-compose.yml +24 -0
- package/docker/ntfy/server.yml +6 -0
- package/package.json +61 -0
- package/scripts/claude-remote.sh +583 -0
- package/scripts/hooks/notify.sh +68 -0
- package/scripts/notify.sh +54 -0
- package/scripts/startup.sh +29 -0
- package/scripts/update-check.sh +25 -0
- package/src/setup-server/public/index.html +21 -0
- package/src/setup-server/public/setup.css +475 -0
- package/src/setup-server/public/setup.js +687 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
/* ============================================================
|
|
2
|
+
RMS DevRemote — Setup Wizard Client
|
|
3
|
+
============================================================ */
|
|
4
|
+
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// State
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
const state = {
|
|
9
|
+
currentStep: 0,
|
|
10
|
+
terminalDomain: '',
|
|
11
|
+
notifyDomain: '',
|
|
12
|
+
tunnelToken: '',
|
|
13
|
+
email: '',
|
|
14
|
+
ttydPassword: '',
|
|
15
|
+
pin: '',
|
|
16
|
+
ntfyTopic: '',
|
|
17
|
+
ntfyPassword: '',
|
|
18
|
+
notifyDomainResult: '',
|
|
19
|
+
prereqsOk: false,
|
|
20
|
+
setupOk: false,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const TOTAL_STEPS = 9;
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// WebSocket
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
let ws = null;
|
|
29
|
+
let wsReady = false;
|
|
30
|
+
|
|
31
|
+
function connectWS() {
|
|
32
|
+
ws = new WebSocket('ws://localhost:3456');
|
|
33
|
+
ws.addEventListener('open', () => { wsReady = true; });
|
|
34
|
+
ws.addEventListener('message', (ev) => {
|
|
35
|
+
try {
|
|
36
|
+
const msg = JSON.parse(ev.data);
|
|
37
|
+
handleWsMessage(msg);
|
|
38
|
+
} catch { /* ignore */ }
|
|
39
|
+
});
|
|
40
|
+
ws.addEventListener('close', () => {
|
|
41
|
+
wsReady = false;
|
|
42
|
+
setTimeout(connectWS, 2000);
|
|
43
|
+
});
|
|
44
|
+
ws.addEventListener('error', () => { ws.close(); });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function handleWsMessage(msg) {
|
|
48
|
+
if (msg.type === 'status') {
|
|
49
|
+
appendLog(msg.data.message, 'normal');
|
|
50
|
+
advanceProgress();
|
|
51
|
+
} else if (msg.type === 'complete') {
|
|
52
|
+
appendLog('Configuration terminee !', 'ok');
|
|
53
|
+
setProgress(100);
|
|
54
|
+
setTimeout(() => goToStep(state.currentStep + 1), 800);
|
|
55
|
+
} else if (msg.type === 'error') {
|
|
56
|
+
appendLog('Erreur: ' + (msg.data.message || 'inconnue'), 'err');
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
// Progress helpers (step 5)
|
|
62
|
+
// ---------------------------------------------------------------------------
|
|
63
|
+
let progressValue = 0;
|
|
64
|
+
let progressIdx = 0;
|
|
65
|
+
const PROGRESS_STEPS_COUNT = 3;
|
|
66
|
+
|
|
67
|
+
function resetProgress() {
|
|
68
|
+
progressValue = 0;
|
|
69
|
+
progressIdx = 0;
|
|
70
|
+
setProgress(0);
|
|
71
|
+
const logEl = document.getElementById('progress-log');
|
|
72
|
+
if (logEl) logEl.textContent = '';
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function advanceProgress() {
|
|
76
|
+
progressIdx++;
|
|
77
|
+
const pct = Math.min(Math.round((progressIdx / PROGRESS_STEPS_COUNT) * 90), 90);
|
|
78
|
+
setProgress(pct);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function setProgress(pct) {
|
|
82
|
+
progressValue = pct;
|
|
83
|
+
const fill = document.getElementById('progress-fill');
|
|
84
|
+
if (fill) fill.style.width = pct + '%';
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function appendLog(text, cls) {
|
|
88
|
+
const logEl = document.getElementById('progress-log');
|
|
89
|
+
if (!logEl) return;
|
|
90
|
+
const line = document.createElement('span');
|
|
91
|
+
line.className = 'log-line' + (cls && cls !== 'normal' ? ' ' + cls : '');
|
|
92
|
+
line.textContent = '> ' + text;
|
|
93
|
+
logEl.appendChild(line);
|
|
94
|
+
logEl.appendChild(document.createTextNode('\n'));
|
|
95
|
+
logEl.scrollTop = logEl.scrollHeight;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
// Clipboard
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
function copyToClipboard(text, btnEl) {
|
|
102
|
+
navigator.clipboard.writeText(text).then(() => {
|
|
103
|
+
const original = btnEl.textContent;
|
|
104
|
+
btnEl.textContent = 'Copie !';
|
|
105
|
+
btnEl.disabled = true;
|
|
106
|
+
setTimeout(() => {
|
|
107
|
+
btnEl.textContent = original;
|
|
108
|
+
btnEl.disabled = false;
|
|
109
|
+
}, 1500);
|
|
110
|
+
}).catch(() => {
|
|
111
|
+
alert('Impossible de copier. Copiez manuellement: ' + text);
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// ---------------------------------------------------------------------------
|
|
116
|
+
// Password generator
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
function generatePassword(length) {
|
|
119
|
+
const len = length || 16;
|
|
120
|
+
const chars = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789!@#$';
|
|
121
|
+
let pwd = '';
|
|
122
|
+
const arr = new Uint8Array(len);
|
|
123
|
+
crypto.getRandomValues(arr);
|
|
124
|
+
arr.forEach(function(b) { pwd += chars[b % chars.length]; });
|
|
125
|
+
return pwd;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ---------------------------------------------------------------------------
|
|
129
|
+
// Step navigation
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
function goToStep(n) {
|
|
132
|
+
if (n < 0 || n >= TOTAL_STEPS) return;
|
|
133
|
+
state.currentStep = n;
|
|
134
|
+
renderStep(n);
|
|
135
|
+
updateStepIndicator(n);
|
|
136
|
+
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function nextStep() { goToStep(state.currentStep + 1); }
|
|
140
|
+
function prevStep() { goToStep(state.currentStep - 1); }
|
|
141
|
+
|
|
142
|
+
// ---------------------------------------------------------------------------
|
|
143
|
+
// Step indicator
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
function updateStepIndicator(active) {
|
|
146
|
+
const dots = document.querySelectorAll('.step-dot');
|
|
147
|
+
const lines = document.querySelectorAll('.step-line');
|
|
148
|
+
dots.forEach(function(dot, i) {
|
|
149
|
+
dot.classList.remove('done', 'active');
|
|
150
|
+
if (i < active) dot.classList.add('done');
|
|
151
|
+
else if (i === active) dot.classList.add('active');
|
|
152
|
+
});
|
|
153
|
+
lines.forEach(function(line, i) {
|
|
154
|
+
line.classList.remove('done');
|
|
155
|
+
if (i < active) line.classList.add('done');
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
// DOM helpers (safe, no innerHTML with user data)
|
|
161
|
+
// ---------------------------------------------------------------------------
|
|
162
|
+
function el(tag, opts) {
|
|
163
|
+
const e = document.createElement(tag);
|
|
164
|
+
if (opts) {
|
|
165
|
+
if (opts.className) e.className = opts.className;
|
|
166
|
+
if (opts.id) e.id = opts.id;
|
|
167
|
+
if (opts.textContent !== undefined) e.textContent = opts.textContent;
|
|
168
|
+
if (opts.type) e.type = opts.type;
|
|
169
|
+
if (opts.placeholder) e.placeholder = opts.placeholder;
|
|
170
|
+
if (opts.value !== undefined) e.value = opts.value;
|
|
171
|
+
if (opts.maxlength) e.maxLength = opts.maxlength;
|
|
172
|
+
if (opts.inputmode) e.inputMode = opts.inputmode;
|
|
173
|
+
if (opts.autocomplete) e.autocomplete = opts.autocomplete;
|
|
174
|
+
if (opts.disabled) e.disabled = true;
|
|
175
|
+
if (opts.href) e.href = opts.href;
|
|
176
|
+
if (opts.target) e.target = opts.target;
|
|
177
|
+
if (opts.style) e.style.cssText = opts.style;
|
|
178
|
+
}
|
|
179
|
+
return e;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function card(title, subtitle) {
|
|
183
|
+
const c = el('div', { className: 'card' });
|
|
184
|
+
c.appendChild(el('h2', { textContent: title }));
|
|
185
|
+
if (subtitle) c.appendChild(el('p', { className: 'subtitle', textContent: subtitle }));
|
|
186
|
+
return c;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function navRow(showBack, nextDisabled) {
|
|
190
|
+
const row = el('div', { className: 'nav-row' });
|
|
191
|
+
const back = el('button', { className: 'btn secondary', id: 'btn-prev', textContent: '\u2190 Retour' });
|
|
192
|
+
if (!showBack) back.style.visibility = 'hidden';
|
|
193
|
+
const next = el('button', { className: 'btn', id: 'btn-next', textContent: 'Suivant \u2192' });
|
|
194
|
+
if (nextDisabled) next.disabled = true;
|
|
195
|
+
row.appendChild(back);
|
|
196
|
+
row.appendChild(next);
|
|
197
|
+
return row;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function infoRow(label, value, copyBtn) {
|
|
201
|
+
const row = el('div', { className: 'info-row' });
|
|
202
|
+
row.appendChild(el('span', { className: 'info-label', textContent: label }));
|
|
203
|
+
row.appendChild(el('span', { className: 'info-value', textContent: value }));
|
|
204
|
+
if (copyBtn) {
|
|
205
|
+
const btn = el('button', { className: 'btn small', textContent: 'Copier' });
|
|
206
|
+
btn.addEventListener('click', function(ev) { copyToClipboard(value, ev.currentTarget); });
|
|
207
|
+
row.appendChild(btn);
|
|
208
|
+
}
|
|
209
|
+
return row;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function alert(type, msg) {
|
|
213
|
+
const a = el('div', { className: 'alert ' + type, textContent: msg });
|
|
214
|
+
return a;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---------------------------------------------------------------------------
|
|
218
|
+
// Render each step
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
function renderStep(n) {
|
|
221
|
+
const container = document.getElementById('step-container');
|
|
222
|
+
if (!container) return;
|
|
223
|
+
while (container.firstChild) container.removeChild(container.firstChild);
|
|
224
|
+
|
|
225
|
+
switch (n) {
|
|
226
|
+
case 0: renderPrereqs(container); break;
|
|
227
|
+
case 1: renderDomains(container); break;
|
|
228
|
+
case 2: renderTunnel(container); break;
|
|
229
|
+
case 3: renderAccess(container); break;
|
|
230
|
+
case 4: renderPassword(container); break;
|
|
231
|
+
case 5: renderDockerBuild(container); break;
|
|
232
|
+
case 6: renderPhoneConfig(container); break;
|
|
233
|
+
case 7: renderPin(container); break;
|
|
234
|
+
case 8: renderSummary(container); break;
|
|
235
|
+
default: break;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// ----- Step 0: Prerequisites -----
|
|
240
|
+
function renderPrereqs(container) {
|
|
241
|
+
const c = card('Prerequis systeme', 'Verification des dependances necessaires au fonctionnement.');
|
|
242
|
+
const list = el('ul', { className: 'prereq-list', id: 'prereq-list' });
|
|
243
|
+
const deps = [['docker','Docker'],['tmux','tmux'],['ttyd','ttyd'],['claude','Claude CLI'],['jq','jq']];
|
|
244
|
+
deps.forEach(function(d) {
|
|
245
|
+
const li = el('li', { id: 'prereq-' + d[0] });
|
|
246
|
+
const icon = el('span', { className: 'prereq-icon pending', id: 'icon-' + d[0], textContent: '\u25CC' });
|
|
247
|
+
li.appendChild(icon);
|
|
248
|
+
li.appendChild(el('span', { textContent: d[1] }));
|
|
249
|
+
list.appendChild(li);
|
|
250
|
+
});
|
|
251
|
+
c.appendChild(list);
|
|
252
|
+
const alertWrap = el('div', { id: 'prereq-alert' });
|
|
253
|
+
c.appendChild(alertWrap);
|
|
254
|
+
const nav = navRow(false, true);
|
|
255
|
+
c.appendChild(nav);
|
|
256
|
+
container.appendChild(c);
|
|
257
|
+
document.getElementById('btn-prev').addEventListener('click', prevStep);
|
|
258
|
+
document.getElementById('btn-next').addEventListener('click', nextStep);
|
|
259
|
+
checkPrereqs();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function checkPrereqs() {
|
|
263
|
+
try {
|
|
264
|
+
const res = await fetch('/api/prereqs');
|
|
265
|
+
const data = await res.json();
|
|
266
|
+
let allOk = true;
|
|
267
|
+
Object.entries(data).forEach(function(entry) {
|
|
268
|
+
const key = entry[0];
|
|
269
|
+
const ok = entry[1];
|
|
270
|
+
const icon = document.getElementById('icon-' + key);
|
|
271
|
+
if (icon) {
|
|
272
|
+
icon.className = 'prereq-icon ' + (ok ? 'ok' : 'fail');
|
|
273
|
+
icon.textContent = ok ? '\u2714' : '\u2718';
|
|
274
|
+
}
|
|
275
|
+
if (!ok) allOk = false;
|
|
276
|
+
});
|
|
277
|
+
state.prereqsOk = allOk;
|
|
278
|
+
const alertEl = document.getElementById('prereq-alert');
|
|
279
|
+
const nextBtn = document.getElementById('btn-next');
|
|
280
|
+
if (allOk) {
|
|
281
|
+
if (alertEl) alertEl.appendChild(alert('success', 'Tous les prerequis sont satisfaits.'));
|
|
282
|
+
if (nextBtn) nextBtn.disabled = false;
|
|
283
|
+
} else {
|
|
284
|
+
if (alertEl) alertEl.appendChild(alert('error', 'Certains outils manquent. Installez-les avant de continuer.'));
|
|
285
|
+
}
|
|
286
|
+
} catch (err) {
|
|
287
|
+
const alertEl = document.getElementById('prereq-alert');
|
|
288
|
+
if (alertEl) alertEl.appendChild(alert('error', 'Impossible de verifier les prerequis.'));
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// ----- Step 1: Domains -----
|
|
293
|
+
function renderDomains(container) {
|
|
294
|
+
const c = card('Domaines', 'Configurez les domaines Cloudflare pour votre terminal et les notifications.');
|
|
295
|
+
function field(labelText, inputId, placeholder, value) {
|
|
296
|
+
const f = el('div', { className: 'field' });
|
|
297
|
+
f.appendChild(el('label', { textContent: labelText }));
|
|
298
|
+
f.appendChild(el('input', { type: 'text', id: inputId, placeholder: placeholder, value: value || '' }));
|
|
299
|
+
return f;
|
|
300
|
+
}
|
|
301
|
+
c.appendChild(field('Domaine terminal (ex: terminal.mondomaine.com)', 'input-terminal-domain', 'terminal.mondomaine.com', state.terminalDomain));
|
|
302
|
+
c.appendChild(field('Domaine notifications ntfy (ex: notify.mondomaine.com)', 'input-notify-domain', 'notify.mondomaine.com', state.notifyDomain));
|
|
303
|
+
const nav = navRow(true, false);
|
|
304
|
+
c.appendChild(nav);
|
|
305
|
+
container.appendChild(c);
|
|
306
|
+
document.getElementById('btn-prev').addEventListener('click', prevStep);
|
|
307
|
+
document.getElementById('btn-next').addEventListener('click', function() {
|
|
308
|
+
const t = document.getElementById('input-terminal-domain').value.trim();
|
|
309
|
+
const n = document.getElementById('input-notify-domain').value.trim();
|
|
310
|
+
if (!t || !n) { window.alert('Veuillez remplir les deux domaines.'); return; }
|
|
311
|
+
state.terminalDomain = t;
|
|
312
|
+
state.notifyDomain = n;
|
|
313
|
+
nextStep();
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// ----- Step 2: Cloudflare Tunnel -----
|
|
318
|
+
function renderTunnel(container) {
|
|
319
|
+
const c = card('Tunnel Cloudflare', 'Creez un tunnel Cloudflare et copiez le token ici.');
|
|
320
|
+
const instr = el('div', { className: 'instructions' });
|
|
321
|
+
const lines = [
|
|
322
|
+
'1. Allez sur one.dash.cloudflare.com',
|
|
323
|
+
'2. Networks → Tunnels → Create a Tunnel',
|
|
324
|
+
'3. Type: Cloudflared, donnez un nom (ex: devremote)',
|
|
325
|
+
'4. Copiez le token (apres --token)',
|
|
326
|
+
'5. Configurez deux Public Hostnames :',
|
|
327
|
+
' - ' + state.terminalDomain + ' → http://localhost:7681',
|
|
328
|
+
' - ' + state.notifyDomain + ' → http://localhost:2586',
|
|
329
|
+
];
|
|
330
|
+
lines.forEach(function(line) {
|
|
331
|
+
instr.appendChild(el('span', { textContent: line }));
|
|
332
|
+
instr.appendChild(document.createElement('br'));
|
|
333
|
+
});
|
|
334
|
+
c.appendChild(instr);
|
|
335
|
+
const f = el('div', { className: 'field' });
|
|
336
|
+
f.appendChild(el('label', { textContent: 'Token Cloudflare Tunnel' }));
|
|
337
|
+
f.appendChild(el('input', { type: 'text', id: 'input-tunnel-token', placeholder: 'eyJ...', value: state.tunnelToken }));
|
|
338
|
+
c.appendChild(f);
|
|
339
|
+
const nav = navRow(true, false);
|
|
340
|
+
c.appendChild(nav);
|
|
341
|
+
container.appendChild(c);
|
|
342
|
+
document.getElementById('btn-prev').addEventListener('click', prevStep);
|
|
343
|
+
document.getElementById('btn-next').addEventListener('click', function() {
|
|
344
|
+
const t = document.getElementById('input-tunnel-token').value.trim();
|
|
345
|
+
if (!t) { window.alert('Veuillez coller le token du tunnel.'); return; }
|
|
346
|
+
state.tunnelToken = t;
|
|
347
|
+
nextStep();
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// ----- Step 3: Cloudflare Access -----
|
|
352
|
+
function renderAccess(container) {
|
|
353
|
+
const c = card('Cloudflare Access', 'Protegez votre terminal avec Cloudflare Access (authentification par email).');
|
|
354
|
+
const instr = el('div', { className: 'instructions' });
|
|
355
|
+
const lines = [
|
|
356
|
+
'1. Dans Cloudflare Zero Trust → Access → Applications → Add an Application',
|
|
357
|
+
'2. Type: Self-hosted',
|
|
358
|
+
'3. Application domain: ' + state.terminalDomain,
|
|
359
|
+
'4. Policy : autorisez votre adresse email',
|
|
360
|
+
'5. Renseignez ci-dessous l\'email autorise',
|
|
361
|
+
];
|
|
362
|
+
lines.forEach(function(line) {
|
|
363
|
+
instr.appendChild(el('span', { textContent: line }));
|
|
364
|
+
instr.appendChild(document.createElement('br'));
|
|
365
|
+
});
|
|
366
|
+
c.appendChild(instr);
|
|
367
|
+
const f = el('div', { className: 'field' });
|
|
368
|
+
f.appendChild(el('label', { textContent: 'Email autorise (Cloudflare Access)' }));
|
|
369
|
+
f.appendChild(el('input', { type: 'email', id: 'input-email', placeholder: 'vous@exemple.com', value: state.email }));
|
|
370
|
+
c.appendChild(f);
|
|
371
|
+
const nav = navRow(true, false);
|
|
372
|
+
c.appendChild(nav);
|
|
373
|
+
container.appendChild(c);
|
|
374
|
+
document.getElementById('btn-prev').addEventListener('click', prevStep);
|
|
375
|
+
document.getElementById('btn-next').addEventListener('click', function() {
|
|
376
|
+
const e = document.getElementById('input-email').value.trim();
|
|
377
|
+
if (!e) { window.alert('Veuillez saisir votre email.'); return; }
|
|
378
|
+
state.email = e;
|
|
379
|
+
nextStep();
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// ----- Step 4: Terminal password -----
|
|
384
|
+
function renderPassword(container) {
|
|
385
|
+
if (!state.ttydPassword) state.ttydPassword = generatePassword(16);
|
|
386
|
+
const c = card('Mot de passe terminal', 'Ce mot de passe protege l\'acces a votre terminal via ttyd.');
|
|
387
|
+
const f = el('div', { className: 'field' });
|
|
388
|
+
f.appendChild(el('label', { textContent: 'Mot de passe ttyd' }));
|
|
389
|
+
const row = el('div', { className: 'input-row' });
|
|
390
|
+
const input = el('input', { type: 'text', id: 'input-ttyd-pwd', value: state.ttydPassword });
|
|
391
|
+
const genBtn = el('button', { className: 'btn secondary small', textContent: 'Generer' });
|
|
392
|
+
const cpyBtn = el('button', { className: 'btn secondary small', textContent: 'Copier' });
|
|
393
|
+
genBtn.addEventListener('click', function() {
|
|
394
|
+
const p = generatePassword(16);
|
|
395
|
+
input.value = p;
|
|
396
|
+
state.ttydPassword = p;
|
|
397
|
+
});
|
|
398
|
+
cpyBtn.addEventListener('click', function(ev) { copyToClipboard(input.value, ev.currentTarget); });
|
|
399
|
+
row.appendChild(input);
|
|
400
|
+
row.appendChild(genBtn);
|
|
401
|
+
row.appendChild(cpyBtn);
|
|
402
|
+
f.appendChild(row);
|
|
403
|
+
c.appendChild(f);
|
|
404
|
+
const nav = navRow(true, false);
|
|
405
|
+
c.appendChild(nav);
|
|
406
|
+
container.appendChild(c);
|
|
407
|
+
document.getElementById('btn-prev').addEventListener('click', prevStep);
|
|
408
|
+
document.getElementById('btn-next').addEventListener('click', function() {
|
|
409
|
+
const p = input.value.trim();
|
|
410
|
+
if (!p) { window.alert('Veuillez definir un mot de passe.'); return; }
|
|
411
|
+
state.ttydPassword = p;
|
|
412
|
+
nextStep();
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// ----- Step 5: Docker build -----
|
|
417
|
+
function renderDockerBuild(container) {
|
|
418
|
+
const c = card('Installation Docker', 'Demarrage des conteneurs (Cloudflare tunnel + ntfy)...');
|
|
419
|
+
const wrap = el('div', { className: 'progress-wrap' });
|
|
420
|
+
const track = el('div', { className: 'progress-bar-track' });
|
|
421
|
+
const fill = el('div', { className: 'progress-bar-fill', id: 'progress-fill' });
|
|
422
|
+
track.appendChild(fill);
|
|
423
|
+
wrap.appendChild(track);
|
|
424
|
+
const log = el('div', { className: 'progress-log', id: 'progress-log' });
|
|
425
|
+
wrap.appendChild(log);
|
|
426
|
+
c.appendChild(wrap);
|
|
427
|
+
const alertWrap = el('div', { id: 'build-alert' });
|
|
428
|
+
c.appendChild(alertWrap);
|
|
429
|
+
const nav = navRow(false, true);
|
|
430
|
+
c.appendChild(nav);
|
|
431
|
+
container.appendChild(c);
|
|
432
|
+
document.getElementById('btn-prev').addEventListener('click', prevStep);
|
|
433
|
+
document.getElementById('btn-next').addEventListener('click', nextStep);
|
|
434
|
+
resetProgress();
|
|
435
|
+
startSetup();
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
async function startSetup() {
|
|
439
|
+
const nextBtn = document.getElementById('btn-next');
|
|
440
|
+
const alertEl = document.getElementById('build-alert');
|
|
441
|
+
try {
|
|
442
|
+
const res = await fetch('/api/setup', {
|
|
443
|
+
method: 'POST',
|
|
444
|
+
headers: { 'Content-Type': 'application/json' },
|
|
445
|
+
body: JSON.stringify({
|
|
446
|
+
terminalDomain: state.terminalDomain,
|
|
447
|
+
notifyDomain: state.notifyDomain,
|
|
448
|
+
tunnelToken: state.tunnelToken,
|
|
449
|
+
ttydPassword: state.ttydPassword,
|
|
450
|
+
email: state.email,
|
|
451
|
+
pin: state.pin,
|
|
452
|
+
}),
|
|
453
|
+
});
|
|
454
|
+
const data = await res.json();
|
|
455
|
+
if (data.ok) {
|
|
456
|
+
state.ntfyTopic = data.ntfyTopic;
|
|
457
|
+
state.ntfyPassword = data.ntfyPassword;
|
|
458
|
+
state.notifyDomainResult = data.notifyDomain;
|
|
459
|
+
state.setupOk = true;
|
|
460
|
+
setProgress(100);
|
|
461
|
+
appendLog('Succes !', 'ok');
|
|
462
|
+
if (nextBtn) nextBtn.disabled = false;
|
|
463
|
+
} else {
|
|
464
|
+
throw new Error(data.error || 'Echec setup');
|
|
465
|
+
}
|
|
466
|
+
} catch (err) {
|
|
467
|
+
if (alertEl) {
|
|
468
|
+
while (alertEl.firstChild) alertEl.removeChild(alertEl.firstChild);
|
|
469
|
+
alertEl.appendChild(alert('error', 'Erreur : ' + err.message));
|
|
470
|
+
}
|
|
471
|
+
appendLog('Echec : ' + err.message, 'err');
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
// ----- Step 6: Phone config -----
|
|
476
|
+
function renderPhoneConfig(container) {
|
|
477
|
+
const domain = state.notifyDomainResult || state.notifyDomain;
|
|
478
|
+
const topic = state.ntfyTopic;
|
|
479
|
+
const password = state.ntfyPassword;
|
|
480
|
+
|
|
481
|
+
const c = card('Configuration telephone', 'Installez l\'application ntfy sur votre telephone et configurez-la.');
|
|
482
|
+
const instr = el('div', { className: 'instructions' });
|
|
483
|
+
const lines = [
|
|
484
|
+
'1. Installez ntfy depuis l\'App Store / Play Store',
|
|
485
|
+
'2. Ajoutez un serveur avec l\'URL ci-dessous',
|
|
486
|
+
'3. Connectez-vous avec le login admin et le mot de passe ci-dessous',
|
|
487
|
+
'4. Abonnez-vous au topic ci-dessous',
|
|
488
|
+
];
|
|
489
|
+
lines.forEach(function(line) {
|
|
490
|
+
instr.appendChild(el('span', { textContent: line }));
|
|
491
|
+
instr.appendChild(document.createElement('br'));
|
|
492
|
+
});
|
|
493
|
+
c.appendChild(instr);
|
|
494
|
+
|
|
495
|
+
const info = el('div', { className: 'info-block' });
|
|
496
|
+
info.appendChild(infoRow('URL serveur', 'https://' + domain, true));
|
|
497
|
+
info.appendChild(infoRow('Login', 'admin', true));
|
|
498
|
+
info.appendChild(infoRow('Mot de passe', password, true));
|
|
499
|
+
info.appendChild(infoRow('Topic', topic, true));
|
|
500
|
+
c.appendChild(info);
|
|
501
|
+
|
|
502
|
+
const alertWrap = el('div', { id: 'notif-alert' });
|
|
503
|
+
c.appendChild(alertWrap);
|
|
504
|
+
|
|
505
|
+
const navR = el('div', { className: 'nav-row' });
|
|
506
|
+
const backBtn = el('button', { className: 'btn secondary', id: 'btn-prev', textContent: '\u2190 Retour' });
|
|
507
|
+
const rightGroup = el('div', { style: 'display:flex;gap:10px' });
|
|
508
|
+
const testBtn = el('button', { className: 'btn secondary', id: 'btn-test', textContent: 'Tester notification' });
|
|
509
|
+
const nextBtn2 = el('button', { className: 'btn', id: 'btn-next', textContent: 'Suivant \u2192' });
|
|
510
|
+
rightGroup.appendChild(testBtn);
|
|
511
|
+
rightGroup.appendChild(nextBtn2);
|
|
512
|
+
navR.appendChild(backBtn);
|
|
513
|
+
navR.appendChild(rightGroup);
|
|
514
|
+
c.appendChild(navR);
|
|
515
|
+
container.appendChild(c);
|
|
516
|
+
|
|
517
|
+
backBtn.addEventListener('click', prevStep);
|
|
518
|
+
nextBtn2.addEventListener('click', nextStep);
|
|
519
|
+
testBtn.addEventListener('click', async function() {
|
|
520
|
+
testBtn.disabled = true;
|
|
521
|
+
testBtn.textContent = 'Envoi...';
|
|
522
|
+
const alertEl = document.getElementById('notif-alert');
|
|
523
|
+
while (alertEl.firstChild) alertEl.removeChild(alertEl.firstChild);
|
|
524
|
+
try {
|
|
525
|
+
const res = await fetch('/api/test-notification', { method: 'POST' });
|
|
526
|
+
const data = await res.json();
|
|
527
|
+
alertEl.appendChild(alert(
|
|
528
|
+
data.ok ? 'success' : 'error',
|
|
529
|
+
data.ok ? 'Notification envoyee ! Verifiez votre telephone.' : 'Echec envoi. Verifiez la configuration ntfy.'
|
|
530
|
+
));
|
|
531
|
+
} catch {
|
|
532
|
+
alertEl.appendChild(alert('error', 'Erreur reseau.'));
|
|
533
|
+
}
|
|
534
|
+
testBtn.disabled = false;
|
|
535
|
+
testBtn.textContent = 'Tester notification';
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// ----- Step 7: PIN -----
|
|
540
|
+
function renderPin(container) {
|
|
541
|
+
const c = card('Code PIN', 'Definissez un code PIN a 6 chiffres pour securiser l\'acces au dashboard.');
|
|
542
|
+
|
|
543
|
+
function pinGroup(id) {
|
|
544
|
+
const f = el('div', { className: 'field' });
|
|
545
|
+
f.appendChild(el('label', { textContent: id === 'pin' ? 'Code PIN (6 chiffres)' : 'Confirmer le PIN' }));
|
|
546
|
+
const g = el('div', { className: 'pin-group', id: id + '-inputs' });
|
|
547
|
+
for (let i = 0; i < 6; i++) {
|
|
548
|
+
g.appendChild(el('input', { type: 'password', inputmode: 'numeric', maxlength: 1, id: id + '-' + i, autocomplete: 'off' }));
|
|
549
|
+
}
|
|
550
|
+
f.appendChild(g);
|
|
551
|
+
return f;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
c.appendChild(pinGroup('pin'));
|
|
555
|
+
c.appendChild(pinGroup('cpin'));
|
|
556
|
+
const alertWrap = el('div', { id: 'pin-alert' });
|
|
557
|
+
c.appendChild(alertWrap);
|
|
558
|
+
const nav = navRow(true, false);
|
|
559
|
+
c.appendChild(nav);
|
|
560
|
+
container.appendChild(c);
|
|
561
|
+
|
|
562
|
+
setupPinInputs('pin', 6);
|
|
563
|
+
setupPinInputs('cpin', 6);
|
|
564
|
+
|
|
565
|
+
document.getElementById('btn-prev').addEventListener('click', prevStep);
|
|
566
|
+
document.getElementById('btn-next').addEventListener('click', function() {
|
|
567
|
+
const pin1 = getPinValue('pin', 6);
|
|
568
|
+
const pin2 = getPinValue('cpin', 6);
|
|
569
|
+
const alertEl = document.getElementById('pin-alert');
|
|
570
|
+
while (alertEl.firstChild) alertEl.removeChild(alertEl.firstChild);
|
|
571
|
+
if (pin1.length !== 6) {
|
|
572
|
+
alertEl.appendChild(alert('error', 'Entrez un PIN de 6 chiffres.'));
|
|
573
|
+
return;
|
|
574
|
+
}
|
|
575
|
+
if (pin1 !== pin2) {
|
|
576
|
+
alertEl.appendChild(alert('error', 'Les PINs ne correspondent pas.'));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
state.pin = pin1;
|
|
580
|
+
nextStep();
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function setupPinInputs(prefix, count) {
|
|
585
|
+
for (let i = 0; i < count; i++) {
|
|
586
|
+
const input = document.getElementById(prefix + '-' + i);
|
|
587
|
+
if (!input) continue;
|
|
588
|
+
(function(idx) {
|
|
589
|
+
input.addEventListener('input', function() {
|
|
590
|
+
input.value = input.value.replace(/\D/g, '').slice(0, 1);
|
|
591
|
+
if (input.value && idx < count - 1) {
|
|
592
|
+
const next = document.getElementById(prefix + '-' + (idx + 1));
|
|
593
|
+
if (next) next.focus();
|
|
594
|
+
}
|
|
595
|
+
});
|
|
596
|
+
input.addEventListener('keydown', function(ev) {
|
|
597
|
+
if (ev.key === 'Backspace' && !input.value && idx > 0) {
|
|
598
|
+
const prev = document.getElementById(prefix + '-' + (idx - 1));
|
|
599
|
+
if (prev) prev.focus();
|
|
600
|
+
}
|
|
601
|
+
});
|
|
602
|
+
})(i);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
function getPinValue(prefix, count) {
|
|
607
|
+
let val = '';
|
|
608
|
+
for (let i = 0; i < count; i++) {
|
|
609
|
+
const inp = document.getElementById(prefix + '-' + i);
|
|
610
|
+
val += (inp ? inp.value : '');
|
|
611
|
+
}
|
|
612
|
+
return val;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// ----- Step 8: Summary -----
|
|
616
|
+
function renderSummary(container) {
|
|
617
|
+
const domain = state.notifyDomainResult || state.notifyDomain;
|
|
618
|
+
const c = card('Configuration terminee !', 'Voici un recapitulatif de votre configuration.');
|
|
619
|
+
|
|
620
|
+
function section(title) {
|
|
621
|
+
const s = el('div', { className: 'summary-section' });
|
|
622
|
+
s.appendChild(el('h3', { textContent: title }));
|
|
623
|
+
const block = el('div', { className: 'info-block' });
|
|
624
|
+
s.appendChild(block);
|
|
625
|
+
return { section: s, block: block };
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
const domains = section('Domaines');
|
|
629
|
+
domains.block.appendChild(infoRow('Terminal', state.terminalDomain, true));
|
|
630
|
+
domains.block.appendChild(infoRow('Notifications', domain, true));
|
|
631
|
+
c.appendChild(domains.section);
|
|
632
|
+
|
|
633
|
+
const terminal = section('Terminal');
|
|
634
|
+
terminal.block.appendChild(infoRow('Email acces', state.email, true));
|
|
635
|
+
terminal.block.appendChild(infoRow('Mot de passe ttyd', state.ttydPassword, true));
|
|
636
|
+
c.appendChild(terminal.section);
|
|
637
|
+
|
|
638
|
+
const ntfy = section('Notifications ntfy');
|
|
639
|
+
ntfy.block.appendChild(infoRow('URL', 'https://' + domain, true));
|
|
640
|
+
ntfy.block.appendChild(infoRow('Login', 'admin', true));
|
|
641
|
+
ntfy.block.appendChild(infoRow('Mot de passe', state.ntfyPassword, true));
|
|
642
|
+
ntfy.block.appendChild(infoRow('Topic', state.ntfyTopic, true));
|
|
643
|
+
c.appendChild(ntfy.section);
|
|
644
|
+
|
|
645
|
+
const successAlert = el('div', { className: 'alert success' });
|
|
646
|
+
successAlert.appendChild(el('span', { textContent: 'Lancez maintenant : ' }));
|
|
647
|
+
successAlert.appendChild(el('strong', { textContent: 'rms-devremote link' }));
|
|
648
|
+
successAlert.appendChild(el('span', { textContent: ' pour demarrer la session !' }));
|
|
649
|
+
c.appendChild(successAlert);
|
|
650
|
+
|
|
651
|
+
const navR = el('div', { className: 'nav-row' });
|
|
652
|
+
navR.appendChild(el('span', {}));
|
|
653
|
+
const finishBtn = el('button', { className: 'btn', id: 'btn-finish', textContent: 'Terminer' });
|
|
654
|
+
navR.appendChild(finishBtn);
|
|
655
|
+
c.appendChild(navR);
|
|
656
|
+
container.appendChild(c);
|
|
657
|
+
|
|
658
|
+
finishBtn.addEventListener('click', async function() {
|
|
659
|
+
try { await fetch('/api/done', { method: 'POST' }); } catch { /* ignore */ }
|
|
660
|
+
finishBtn.textContent = 'Fermeture...';
|
|
661
|
+
finishBtn.disabled = true;
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
// ---------------------------------------------------------------------------
|
|
666
|
+
// Build step indicator DOM
|
|
667
|
+
// ---------------------------------------------------------------------------
|
|
668
|
+
function buildStepIndicator() {
|
|
669
|
+
const wrap = document.getElementById('step-indicator');
|
|
670
|
+
if (!wrap) return;
|
|
671
|
+
for (let i = 0; i < TOTAL_STEPS; i++) {
|
|
672
|
+
const dot = el('div', { className: 'step-dot', textContent: String(i + 1) });
|
|
673
|
+
wrap.appendChild(dot);
|
|
674
|
+
if (i < TOTAL_STEPS - 1) {
|
|
675
|
+
wrap.appendChild(el('div', { className: 'step-line' }));
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
// ---------------------------------------------------------------------------
|
|
681
|
+
// Init
|
|
682
|
+
// ---------------------------------------------------------------------------
|
|
683
|
+
document.addEventListener('DOMContentLoaded', function() {
|
|
684
|
+
connectWS();
|
|
685
|
+
buildStepIndicator();
|
|
686
|
+
goToStep(0);
|
|
687
|
+
});
|