@imenam/self-register-proxy 1.0.37

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.
@@ -0,0 +1,688 @@
1
+ const { escapeHtml } = require('../services/proxyRouter');
2
+
3
+ /**
4
+ * Page dashboard principale
5
+ * @param {Array} routes - liste des routes enregistrées
6
+ * @param {number} availablePorts - nombre de ports disponibles
7
+ * @param {string} proxyName
8
+ * @param {boolean} hasAuth - true si l'authentification est activée
9
+ * @param {string|undefined} cleanUpCommand - si défini, affiche le bouton de nettoyage
10
+ */
11
+ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpCommand) {
12
+ const servicesHtml = routes.length > 0
13
+ ? `<div class="services-list">
14
+ ${routes.map(route => `
15
+ <div class="service-card" data-path="${escapeHtml(route.path)}">
16
+ <div class="service-info">
17
+ <span class="service-name">${escapeHtml(route.name)}</span>
18
+ <span class="service-path">${escapeHtml(route.path)}</span>
19
+ <span class="service-target">&rarr; ${escapeHtml(route.target)}:${route.port}</span>
20
+ <span class="service-health">${route.healthCheck !== false ? '✅' : '⚪'}</span>
21
+ <span class="service-config"></span>
22
+ </div>
23
+ <div class="service-actions">
24
+ <a href="${escapeHtml(route.path)}" class="action-btn primary" title="Acc&eacute;der">Ouvrir</a>
25
+ <button onclick="checkHealth('${escapeHtml(route.path)}', this)" class="action-btn" title="V&eacute;rifier la sant&eacute;">Sant&eacute;</button>
26
+ <button onclick="killPort(${route.port}, this)" class="action-btn danger" title="Tuer le process">Tuer</button>
27
+ </div>
28
+ </div>
29
+ `).join('')}
30
+ </div>`
31
+ : `<div class="empty-state">
32
+ <h2>Aucun service enregistr&eacute;</h2>
33
+ <p>Cliquez sur "+ Ajouter" ou utilisez l'API:</p>
34
+ <p><span class="code">POST /proxy/register-manual</span></p>
35
+ </div>`;
36
+
37
+ return `<!DOCTYPE html>
38
+ <html lang="fr">
39
+ <head>
40
+ <meta charset="UTF-8">
41
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
42
+ <title>${escapeHtml(proxyName)} - Services disponibles</title>
43
+ <style>
44
+ * {
45
+ margin: 0;
46
+ padding: 0;
47
+ box-sizing: border-box;
48
+ }
49
+ body {
50
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
51
+ background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
52
+ min-height: 100vh;
53
+ color: #e4e4e7;
54
+ padding: 1.5rem;
55
+ }
56
+ .container {
57
+ max-width: 1100px;
58
+ margin: 0 auto;
59
+ }
60
+ header {
61
+ text-align: center;
62
+ margin-bottom: 1.5rem;
63
+ }
64
+ h1 {
65
+ font-size: 2rem;
66
+ background: linear-gradient(90deg, #6366f1, #8b5cf6);
67
+ -webkit-background-clip: text;
68
+ -webkit-text-fill-color: transparent;
69
+ background-clip: text;
70
+ margin-bottom: 0.25rem;
71
+ }
72
+ .subtitle {
73
+ color: #a1a1aa;
74
+ font-size: 0.95rem;
75
+ }
76
+ .stats {
77
+ display: flex;
78
+ justify-content: center;
79
+ gap: 1.5rem;
80
+ margin-top: 1rem;
81
+ }
82
+ .stat {
83
+ background: rgba(255, 255, 255, 0.05);
84
+ padding: 0.5rem 1rem;
85
+ border-radius: 8px;
86
+ text-align: center;
87
+ }
88
+ .stat-value {
89
+ font-size: 1.25rem;
90
+ font-weight: bold;
91
+ color: #8b5cf6;
92
+ }
93
+ .stat-label {
94
+ font-size: 0.75rem;
95
+ color: #71717a;
96
+ }
97
+ .toolbar {
98
+ display: flex;
99
+ justify-content: space-between;
100
+ align-items: center;
101
+ margin-bottom: 0.75rem;
102
+ gap: 0.5rem;
103
+ }
104
+ .toolbar-left {
105
+ display: flex;
106
+ gap: 0.5rem;
107
+ align-items: center;
108
+ }
109
+ .toolbar-right {
110
+ display: flex;
111
+ gap: 0.5rem;
112
+ align-items: center;
113
+ }
114
+ .agent-btn {
115
+ background: linear-gradient(90deg, #10b981, #059669);
116
+ color: white;
117
+ border: none;
118
+ padding: 0.5rem 1rem;
119
+ border-radius: 6px;
120
+ font-weight: 600;
121
+ font-size: 0.8rem;
122
+ cursor: pointer;
123
+ transition: opacity 0.2s;
124
+ display: inline-flex;
125
+ align-items: center;
126
+ gap: 0.4rem;
127
+ white-space: nowrap;
128
+ }
129
+ .agent-btn:hover {
130
+ opacity: 0.9;
131
+ }
132
+ .cleanup-btn {
133
+ background: linear-gradient(90deg, #ef4444, #dc2626);
134
+ color: white;
135
+ border: none;
136
+ padding: 0.5rem 1rem;
137
+ border-radius: 6px;
138
+ font-weight: 600;
139
+ font-size: 0.8rem;
140
+ cursor: pointer;
141
+ transition: opacity 0.2s;
142
+ display: inline-flex;
143
+ align-items: center;
144
+ gap: 0.4rem;
145
+ white-space: nowrap;
146
+ }
147
+ .cleanup-btn:hover { opacity: 0.9; }
148
+ .cleanup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
149
+ .add-btn {
150
+ background: linear-gradient(90deg, #6366f1, #8b5cf6);
151
+ color: white;
152
+ border: none;
153
+ padding: 0.5rem 1rem;
154
+ border-radius: 6px;
155
+ font-weight: 600;
156
+ font-size: 0.8rem;
157
+ cursor: pointer;
158
+ transition: opacity 0.2s;
159
+ display: inline-flex;
160
+ align-items: center;
161
+ gap: 0.4rem;
162
+ }
163
+ .add-btn:hover {
164
+ opacity: 0.9;
165
+ }
166
+ .modal-overlay {
167
+ display: none;
168
+ position: fixed;
169
+ top: 0;
170
+ left: 0;
171
+ right: 0;
172
+ bottom: 0;
173
+ background: rgba(0, 0, 0, 0.6);
174
+ z-index: 1000;
175
+ align-items: center;
176
+ justify-content: center;
177
+ }
178
+ .modal-overlay.active {
179
+ display: flex;
180
+ }
181
+ .modal {
182
+ background: linear-gradient(135deg, #1e1e3f 0%, #1a1a2e 100%);
183
+ border: 1px solid rgba(255, 255, 255, 0.15);
184
+ border-radius: 12px;
185
+ padding: 1.5rem;
186
+ width: 100%;
187
+ max-width: 360px;
188
+ box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
189
+ }
190
+ .modal-header {
191
+ display: flex;
192
+ justify-content: space-between;
193
+ align-items: center;
194
+ margin-bottom: 1rem;
195
+ }
196
+ .modal-header h2 {
197
+ font-size: 1.1rem;
198
+ color: #f4f4f5;
199
+ }
200
+ .modal-close {
201
+ background: transparent;
202
+ border: none;
203
+ color: #71717a;
204
+ font-size: 1.25rem;
205
+ cursor: pointer;
206
+ padding: 0.25rem;
207
+ line-height: 1;
208
+ }
209
+ .modal-close:hover {
210
+ color: #e4e4e7;
211
+ }
212
+ .modal .form-group {
213
+ margin-bottom: 0.75rem;
214
+ }
215
+ .modal .form-group label {
216
+ font-size: 0.75rem;
217
+ color: #a1a1aa;
218
+ margin-bottom: 0.25rem;
219
+ display: block;
220
+ }
221
+ .modal .form-group input {
222
+ background: rgba(0, 0, 0, 0.3);
223
+ border: 1px solid rgba(255, 255, 255, 0.1);
224
+ padding: 0.5rem 0.75rem;
225
+ border-radius: 6px;
226
+ color: white;
227
+ font-family: inherit;
228
+ font-size: 0.9rem;
229
+ width: 100%;
230
+ }
231
+ .modal .form-group input:focus {
232
+ outline: none;
233
+ border-color: #8b5cf6;
234
+ background: rgba(0, 0, 0, 0.4);
235
+ }
236
+ .modal .checkbox-group {
237
+ display: flex;
238
+ align-items: center;
239
+ gap: 0.5rem;
240
+ margin-bottom: 1rem;
241
+ }
242
+ .modal .checkbox-group input {
243
+ width: auto;
244
+ }
245
+ .modal .checkbox-group label {
246
+ font-size: 0.75rem;
247
+ color: #a1a1aa;
248
+ margin: 0;
249
+ }
250
+ .modal .submit-btn {
251
+ background: linear-gradient(90deg, #6366f1, #8b5cf6);
252
+ color: white;
253
+ border: none;
254
+ padding: 0.6rem 1.25rem;
255
+ border-radius: 6px;
256
+ font-weight: 600;
257
+ font-size: 0.85rem;
258
+ cursor: pointer;
259
+ transition: opacity 0.2s;
260
+ width: 100%;
261
+ }
262
+ .modal .submit-btn:hover {
263
+ opacity: 0.9;
264
+ }
265
+ .services-list {
266
+ display: flex;
267
+ flex-direction: column;
268
+ gap: 0.5rem;
269
+ }
270
+ .service-card {
271
+ background: rgba(255, 255, 255, 0.05);
272
+ border: 1px solid rgba(255, 255, 255, 0.1);
273
+ border-radius: 8px;
274
+ padding: 0.6rem 0.9rem;
275
+ display: flex;
276
+ align-items: center;
277
+ justify-content: space-between;
278
+ gap: 0.75rem;
279
+ transition: all 0.15s ease;
280
+ }
281
+ .service-card:hover {
282
+ background: rgba(255, 255, 255, 0.08);
283
+ border-color: rgba(139, 92, 246, 0.4);
284
+ }
285
+ .service-info {
286
+ display: flex;
287
+ align-items: center;
288
+ gap: 0.6rem;
289
+ flex-wrap: wrap;
290
+ min-width: 0;
291
+ flex: 1;
292
+ }
293
+ .service-name {
294
+ font-size: 0.95rem;
295
+ font-weight: 600;
296
+ color: #f4f4f5;
297
+ white-space: nowrap;
298
+ }
299
+ .service-path {
300
+ font-family: 'SF Mono', 'Fira Code', monospace;
301
+ font-size: 0.75rem;
302
+ color: #8b5cf6;
303
+ background: rgba(139, 92, 246, 0.15);
304
+ padding: 0.15rem 0.5rem;
305
+ border-radius: 4px;
306
+ white-space: nowrap;
307
+ }
308
+ .service-target {
309
+ font-size: 0.75rem;
310
+ color: #71717a;
311
+ white-space: nowrap;
312
+ }
313
+ .service-health {
314
+ font-size: 0.7rem;
315
+ }
316
+ .service-config {
317
+ font-size: 0.7rem;
318
+ color: #10b981;
319
+ background: rgba(16, 185, 129, 0.1);
320
+ border: 1px solid rgba(16, 185, 129, 0.25);
321
+ padding: 0.1rem 0.45rem;
322
+ border-radius: 4px;
323
+ white-space: nowrap;
324
+ }
325
+ .service-config:empty { display: none; }
326
+ .service-actions {
327
+ display: flex;
328
+ gap: 0.4rem;
329
+ flex-shrink: 0;
330
+ align-items: center;
331
+ }
332
+ .action-btn {
333
+ background: rgba(255, 255, 255, 0.1);
334
+ color: #e4e4e7;
335
+ border: 1px solid rgba(255, 255, 255, 0.15);
336
+ padding: 0.35rem 0.6rem;
337
+ border-radius: 5px;
338
+ cursor: pointer;
339
+ font-size: 0.75rem;
340
+ font-weight: 500;
341
+ transition: all 0.15s;
342
+ text-decoration: none;
343
+ display: inline-flex;
344
+ align-items: center;
345
+ }
346
+ .action-btn:hover {
347
+ background: rgba(255, 255, 255, 0.15);
348
+ border-color: rgba(255, 255, 255, 0.25);
349
+ }
350
+ .action-btn.primary {
351
+ background: linear-gradient(90deg, #6366f1, #8b5cf6);
352
+ border: none;
353
+ color: white;
354
+ }
355
+ .action-btn.primary:hover {
356
+ opacity: 0.9;
357
+ }
358
+ .action-btn.loading {
359
+ opacity: 0.5;
360
+ cursor: not-allowed;
361
+ }
362
+ .action-btn.healthy {
363
+ background: rgba(16, 185, 129, 0.2);
364
+ border-color: rgba(16, 185, 129, 0.4);
365
+ color: #10b981;
366
+ }
367
+ .action-btn.unhealthy {
368
+ background: rgba(239, 68, 68, 0.2);
369
+ border-color: rgba(239, 68, 68, 0.4);
370
+ color: #ef4444;
371
+ }
372
+ .action-btn.danger {
373
+ background: rgba(239, 68, 68, 0.15);
374
+ border-color: rgba(239, 68, 68, 0.35);
375
+ color: #ef4444;
376
+ }
377
+ .action-btn.danger:hover {
378
+ background: rgba(239, 68, 68, 0.25);
379
+ border-color: rgba(239, 68, 68, 0.5);
380
+ }
381
+ .empty-state {
382
+ text-align: center;
383
+ padding: 2.5rem 1.5rem;
384
+ background: rgba(255, 255, 255, 0.03);
385
+ border-radius: 10px;
386
+ border: 1px dashed rgba(255, 255, 255, 0.1);
387
+ }
388
+ .empty-state h2 {
389
+ color: #a1a1aa;
390
+ font-size: 1rem;
391
+ margin-bottom: 0.5rem;
392
+ }
393
+ .empty-state p {
394
+ color: #71717a;
395
+ font-size: 0.85rem;
396
+ margin-bottom: 0.25rem;
397
+ }
398
+ .code {
399
+ font-family: 'SF Mono', 'Fira Code', monospace;
400
+ background: rgba(0, 0, 0, 0.3);
401
+ padding: 0.15rem 0.4rem;
402
+ border-radius: 4px;
403
+ font-size: 0.8rem;
404
+ }
405
+ footer {
406
+ margin-top: 1.5rem;
407
+ text-align: center;
408
+ color: #52525b;
409
+ font-size: 0.75rem;
410
+ }
411
+ footer a {
412
+ color: #8b5cf6;
413
+ text-decoration: none;
414
+ }
415
+ footer a:hover {
416
+ text-decoration: underline;
417
+ }
418
+ @media (max-width: 600px) {
419
+ .toolbar {
420
+ flex-direction: column;
421
+ }
422
+ .toolbar-right {
423
+ width: 100%;
424
+ justify-content: flex-end;
425
+ }
426
+ .service-card {
427
+ flex-direction: column;
428
+ align-items: flex-start;
429
+ gap: 0.5rem;
430
+ }
431
+ .service-actions {
432
+ width: 100%;
433
+ justify-content: flex-end;
434
+ }
435
+ .modal {
436
+ margin: 1rem;
437
+ max-width: none;
438
+ }
439
+ }
440
+ </style>
441
+ </head>
442
+ <body>
443
+ <div class="container">
444
+ <header>
445
+ <h1>🚀 ${escapeHtml(proxyName)}</h1>
446
+ <p class="subtitle">Services disponibles</p>
447
+ <div class="stats">
448
+ <div class="stat">
449
+ <div class="stat-value">${routes.length}</div>
450
+ <div class="stat-label">Services actifs</div>
451
+ </div>
452
+ <div class="stat">
453
+ <div class="stat-value">${availablePorts}</div>
454
+ <div class="stat-label">Ports disponibles</div>
455
+ </div>
456
+ </div>
457
+ </header>
458
+
459
+ <div class="toolbar">
460
+ <div class="toolbar-left">
461
+ <button class="agent-btn" onclick="openAgent()">&#9654; Lancer Agent</button>
462
+ ${cleanUpCommand ? '<button class="cleanup-btn" id="cleanupBtn" onclick="runCleanUp()">&#128465; Nettoyer les process</button>' : ''}
463
+ </div>
464
+ <div class="toolbar-right">
465
+ <a href="/proxy/meta-configs-ui" class="add-btn" style="text-decoration:none;">&#9881; Meta Configs</a>
466
+ <button class="action-btn" id="refreshConfigsBtn" onclick="loadActiveConfigs()">&#10227; Configs</button>
467
+ <button class="add-btn" onclick="openModal()">+ Ajouter</button>
468
+ ${hasAuth ? '<button class="action-btn" onclick="logout()">D&eacute;connexion</button>' : ''}
469
+ </div>
470
+ </div>
471
+
472
+ ${servicesHtml}
473
+
474
+ <div class="modal-overlay" id="modalOverlay" onclick="closeModalOnOverlay(event)">
475
+ <div class="modal">
476
+ <div class="modal-header">
477
+ <h2>Nouveau service</h2>
478
+ <button class="modal-close" onclick="closeModal()">&times;</button>
479
+ </div>
480
+ <form id="registerForm">
481
+ <div class="form-group">
482
+ <label for="path">Chemin</label>
483
+ <input type="text" id="path" name="path" placeholder="/mon-app" required>
484
+ </div>
485
+ <div class="form-group">
486
+ <label for="name">Nom</label>
487
+ <input type="text" id="name" name="name" placeholder="Mon Application">
488
+ </div>
489
+ <div class="form-group">
490
+ <label for="target">H&ocirc;te cible</label>
491
+ <input type="text" id="target" name="target" placeholder="http://localhost" value="http://localhost">
492
+ </div>
493
+ <div class="form-group">
494
+ <label for="port">Port</label>
495
+ <input type="number" id="port" name="port" placeholder="4000" required>
496
+ </div>
497
+ <div class="checkbox-group">
498
+ <input type="checkbox" id="healthCheck" name="healthCheck">
499
+ <label for="healthCheck">Health check actif</label>
500
+ </div>
501
+ <button type="submit" class="submit-btn" id="submitBtn">Enregistrer</button>
502
+ </form>
503
+ </div>
504
+ </div>
505
+
506
+ <footer>
507
+ <p>API: <a href="/proxy/routes">/proxy/routes</a> | <a href="/proxy/health">/proxy/health</a></p>
508
+ </footer>
509
+ </div>
510
+
511
+ <script>
512
+ async function loadActiveConfigs() {
513
+ const btn = document.getElementById('refreshConfigsBtn');
514
+ if (btn) { btn.disabled = true; btn.textContent = '\u2026'; }
515
+ try {
516
+ const res = await fetch('/proxy/mcp-configurations');
517
+ const data = await res.json();
518
+ (data.results || []).forEach(mcp => {
519
+ const card = document.querySelector('.service-card[data-path="' + mcp.path + '"]');
520
+ if (!card) return;
521
+ const span = card.querySelector('.service-config');
522
+ if (!mcp.configurations) { span.textContent = ''; return; }
523
+ const active = mcp.configurations.find(c => c.active);
524
+ span.textContent = active ? (active.name || active.id) : '';
525
+ });
526
+ } catch (_) {} finally {
527
+ if (btn) { btn.disabled = false; btn.textContent = '\u21ba Configs'; }
528
+ }
529
+ }
530
+ loadActiveConfigs();
531
+
532
+ function openModal() {
533
+ document.getElementById('modalOverlay').classList.add('active');
534
+ document.getElementById('path').focus();
535
+ }
536
+
537
+ function closeModal() {
538
+ document.getElementById('modalOverlay').classList.remove('active');
539
+ document.getElementById('registerForm').reset();
540
+ document.getElementById('target').value = 'http://localhost';
541
+ }
542
+
543
+ function closeModalOnOverlay(event) {
544
+ if (event.target === document.getElementById('modalOverlay')) {
545
+ closeModal();
546
+ }
547
+ }
548
+
549
+ document.addEventListener('keydown', (e) => {
550
+ if (e.key === 'Escape') closeModal();
551
+ });
552
+
553
+ document.getElementById('registerForm').addEventListener('submit', async (e) => {
554
+ e.preventDefault();
555
+ const btn = document.getElementById('submitBtn');
556
+ const originalText = btn.innerText;
557
+
558
+ const formData = {
559
+ path: document.getElementById('path').value,
560
+ name: document.getElementById('name').value,
561
+ target: document.getElementById('target').value,
562
+ port: parseInt(document.getElementById('port').value),
563
+ healthCheck: document.getElementById('healthCheck').checked
564
+ };
565
+
566
+ btn.innerText = 'Enregistrement...';
567
+ btn.disabled = true;
568
+
569
+ try {
570
+ const response = await fetch('/proxy/register-manual', {
571
+ method: 'POST',
572
+ headers: { 'Content-Type': 'application/json' },
573
+ body: JSON.stringify(formData)
574
+ });
575
+
576
+ const data = await response.json();
577
+
578
+ if (response.ok) {
579
+ btn.innerText = '\u2705 Succ\u00e8s\u00a0!';
580
+ setTimeout(() => {
581
+ window.location.reload();
582
+ }, 1000);
583
+ } else {
584
+ alert('Erreur: ' + data.error);
585
+ btn.innerText = originalText;
586
+ btn.disabled = false;
587
+ }
588
+ } catch (error) {
589
+ alert('Erreur r\u00e9seau');
590
+ btn.innerText = originalText;
591
+ btn.disabled = false;
592
+ }
593
+ });
594
+
595
+ function openAgent() {
596
+ window.open('/proxy/terminal', '_blank');
597
+ }
598
+
599
+ async function runCleanUp() {
600
+ if (!confirm('Nettoyer tous les processus ?')) return;
601
+ const btn = document.getElementById('cleanupBtn');
602
+ const originalText = btn.innerHTML;
603
+ btn.disabled = true;
604
+ btn.innerHTML = '&#8987; Nettoyage\u2026';
605
+ try {
606
+ await fetch('/proxy/cleanup', { method: 'POST' });
607
+ btn.innerHTML = '&#10003; Nettoy\u00e9';
608
+ setTimeout(() => { btn.innerHTML = originalText; btn.disabled = false; }, 3000);
609
+ } catch (_) {
610
+ btn.innerHTML = originalText;
611
+ btn.disabled = false;
612
+ }
613
+ }
614
+
615
+ function logout() {
616
+ const form = document.createElement('form');
617
+ form.method = 'POST';
618
+ form.action = '/proxy/logout';
619
+ document.body.appendChild(form);
620
+ form.submit();
621
+ }
622
+
623
+ async function killPort(port, btn) {
624
+ if (btn.classList.contains('loading')) return;
625
+ if (!confirm('Tuer le process sur le port ' + port + ' ?')) return;
626
+ const originalText = btn.innerText;
627
+ btn.innerText = '\u2026';
628
+ btn.classList.add('loading');
629
+ try {
630
+ await fetch('/proxy/kill-port', {
631
+ method: 'POST',
632
+ headers: { 'Content-Type': 'application/json' },
633
+ body: JSON.stringify({ port })
634
+ });
635
+ btn.innerText = '\u2713 Tu\u00e9';
636
+ setTimeout(() => { btn.innerText = originalText; btn.classList.remove('loading'); }, 2000);
637
+ } catch (_) {
638
+ btn.innerText = originalText;
639
+ btn.classList.remove('loading');
640
+ }
641
+ }
642
+
643
+ async function checkHealth(path, btn) {
644
+ if (btn.classList.contains('loading')) return;
645
+
646
+ const originalText = btn.innerText;
647
+ btn.innerText = 'V\u00e9rification...';
648
+ btn.classList.add('loading');
649
+
650
+ try {
651
+ const response = await fetch('/proxy/check', {
652
+ method: 'POST',
653
+ headers: { 'Content-Type': 'application/json' },
654
+ body: JSON.stringify({ path })
655
+ });
656
+
657
+ const data = await response.json();
658
+
659
+ if (data.healthy) {
660
+ btn.innerText = '\u2705 Sain';
661
+ btn.classList.remove('loading');
662
+ btn.classList.add('healthy');
663
+ setTimeout(() => {
664
+ btn.innerText = originalText;
665
+ btn.classList.remove('healthy');
666
+ }, 3000);
667
+ } else {
668
+ btn.innerText = '\u274c Hors ligne';
669
+ btn.classList.remove('loading');
670
+ btn.classList.add('unhealthy');
671
+ setTimeout(() => {
672
+ window.location.reload();
673
+ }, 2000);
674
+ }
675
+ } catch (error) {
676
+ btn.innerText = '\u26a0\ufe0f Erreur';
677
+ btn.classList.remove('loading');
678
+ setTimeout(() => {
679
+ btn.innerText = originalText;
680
+ }, 3000);
681
+ }
682
+ }
683
+ </script>
684
+ </body>
685
+ </html>`;
686
+ }
687
+
688
+ module.exports = { renderDashboard };