@imenam/self-register-proxy 1.0.42 → 1.0.43
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/package.json +1 -1
- package/routes/metaConfig.js +2 -2
- package/views/dashboard.js +309 -803
- package/views/login.js +61 -130
- package/views/metaConfigs.js +403 -643
- package/views/terminal.html +272 -691
- package/views/terminal.js +34 -30
- package/views/theme.js +431 -0
package/views/dashboard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const { escapeHtml } = require('../services/proxyRouter');
|
|
2
|
+
const { BASE_CSS, SHELL_CSS, SHELL_JS, renderSidebar, icon } = require('./theme');
|
|
2
3
|
|
|
3
4
|
/** Libellé de la section pour les applications sans groupe (APP_GROUP absent). */
|
|
4
5
|
const UNGROUPED_LABEL = 'Non groupées';
|
|
@@ -26,38 +27,58 @@ function groupRoutes(routes) {
|
|
|
26
27
|
});
|
|
27
28
|
}
|
|
28
29
|
|
|
30
|
+
/** Nom du groupe d'une route, jamais vide. */
|
|
31
|
+
function groupOf(route) {
|
|
32
|
+
return (route.group && String(route.group).trim()) || UNGROUPED_LABEL;
|
|
33
|
+
}
|
|
34
|
+
|
|
29
35
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* les cartes sont rendues à plat (aucun groupe explicite à afficher).
|
|
33
|
-
* @param {Array} routes
|
|
34
|
-
* @param {(route: any) => string} renderCard
|
|
35
|
-
* @returns {string}
|
|
36
|
+
* Filtres de groupe du panneau latéral. Le groupe est un filtre, pas une
|
|
37
|
+
* section repliable : la liste reste à plat quel que soit le filtre actif.
|
|
36
38
|
*/
|
|
37
|
-
function
|
|
39
|
+
function renderGroupFilters(routes) {
|
|
38
40
|
const groups = groupRoutes(routes);
|
|
39
|
-
|
|
41
|
+
if (!groups.length) return '';
|
|
40
42
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
43
|
+
const item = (label, value, count, extraClass) => `
|
|
44
|
+
<button class="nav-item nav-item-plain${extraClass || ''}" data-filter="${escapeHtml(value)}" onclick="setGroupFilter(this)">
|
|
45
|
+
<span class="nav-item-label">${escapeHtml(label)}</span>
|
|
46
|
+
<span class="nav-count">${count}</span>
|
|
47
|
+
</button>`;
|
|
46
48
|
|
|
47
|
-
return
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
49
|
+
return item('Tous', '', routes.length, ' active')
|
|
50
|
+
+ groups.map(g => item(g.name, g.name, g.routes.length, g.name === UNGROUPED_LABEL ? ' nav-item-muted' : '')).join('');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Une ligne de service : santé, identité, groupe, ouverture, menu d'actions. */
|
|
54
|
+
function renderServiceRow(route) {
|
|
55
|
+
const group = groupOf(route);
|
|
56
|
+
const fullTarget = `${route.target}:${route.port}`;
|
|
57
|
+
const healthy = route.healthCheck !== false;
|
|
58
|
+
const searchable = `${route.name} ${route.path} ${fullTarget} ${group}`.toLowerCase();
|
|
59
|
+
|
|
60
|
+
return `
|
|
61
|
+
<div class="row" data-path="${escapeHtml(route.path)}" data-port="${route.port}" data-group="${escapeHtml(group)}" data-search="${escapeHtml(searchable)}">
|
|
62
|
+
<span class="dot ${healthy ? 'up' : ''}" title="${healthy ? 'Health check actif' : 'Sans health check'}"></span>
|
|
63
|
+
<div class="row-main">
|
|
64
|
+
<div class="row-name">${escapeHtml(route.name)}</div>
|
|
65
|
+
<div class="row-meta">
|
|
66
|
+
<span class="row-detail">${escapeHtml(route.path)} · ${escapeHtml(fullTarget)}</span>
|
|
67
|
+
</div>
|
|
57
68
|
</div>
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
69
|
+
<span class="tag row-config hidden"></span>
|
|
70
|
+
<span class="tag${group === UNGROUPED_LABEL ? ' tag-muted' : ''}">${escapeHtml(group)}</span>
|
|
71
|
+
<a class="row-open" href="${escapeHtml(route.path)}" target="_blank" rel="noopener noreferrer">Ouvrir</a>
|
|
72
|
+
<div class="menu-wrap">
|
|
73
|
+
<button class="icon-btn" onclick="toggleMenu(this)" title="Actions" aria-label="Actions">${icon('more')}</button>
|
|
74
|
+
<div class="menu">
|
|
75
|
+
<button class="menu-item" onclick="checkHealth(this)">${icon('activity')}<span>Vérifier la santé</span></button>
|
|
76
|
+
<button class="menu-item" onclick="copyUrl(this)">${icon('copy')}<span>Copier l'URL</span></button>
|
|
77
|
+
<div class="menu-sep"></div>
|
|
78
|
+
<button class="menu-item danger" onclick="killPort(this)">${icon('circleX')}<span>Tuer le process</span></button>
|
|
79
|
+
</div>
|
|
80
|
+
</div>
|
|
81
|
+
</div>`;
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
/**
|
|
@@ -66,719 +87,239 @@ function renderGroupedServices(routes, renderCard) {
|
|
|
66
87
|
* @param {number} availablePorts - nombre de ports disponibles
|
|
67
88
|
* @param {string} proxyName
|
|
68
89
|
* @param {boolean} hasAuth - true si l'authentification est activée
|
|
69
|
-
* @param {string|undefined} cleanUpCommand - si défini, affiche
|
|
90
|
+
* @param {string|undefined} cleanUpCommand - si défini, affiche l'action de nettoyage
|
|
70
91
|
* @param {boolean} [minimalDashboard] - interface HTML réduite (nom + Ouvrir uniquement)
|
|
71
92
|
*/
|
|
72
93
|
function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpCommand, minimalDashboard) {
|
|
73
|
-
if (minimalDashboard)
|
|
74
|
-
const renderMinimalCard = (route) => `
|
|
75
|
-
<div class="service-card">
|
|
76
|
-
<div class="service-info">
|
|
77
|
-
<span class="service-name">${escapeHtml(route.name)}</span>
|
|
78
|
-
</div>
|
|
79
|
-
<div class="service-actions">
|
|
80
|
-
<a href="${escapeHtml(route.path)}" class="action-btn primary" title="Accéder" target="_blank" rel="noopener noreferrer">Ouvrir</a>
|
|
81
|
-
</div>
|
|
82
|
-
</div>
|
|
83
|
-
`;
|
|
84
|
-
|
|
85
|
-
const servicesHtml = routes.length > 0
|
|
86
|
-
? renderGroupedServices(routes, renderMinimalCard)
|
|
87
|
-
: `<div class="empty-state minimal-empty">
|
|
88
|
-
<p>Aucun service</p>
|
|
89
|
-
</div>`;
|
|
90
|
-
|
|
91
|
-
return `<!DOCTYPE html>
|
|
92
|
-
<html lang="fr">
|
|
93
|
-
<head>
|
|
94
|
-
<meta charset="UTF-8">
|
|
95
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
96
|
-
<title>${escapeHtml(proxyName)}</title>
|
|
97
|
-
<style>
|
|
98
|
-
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
99
|
-
body.minimal-dashboard {
|
|
100
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
101
|
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
102
|
-
min-height: 100vh;
|
|
103
|
-
color: #e4e4e7;
|
|
104
|
-
padding: 1rem;
|
|
105
|
-
}
|
|
106
|
-
.minimal-dashboard .container { max-width: 560px; margin: 0 auto; }
|
|
107
|
-
.minimal-dashboard header { text-align: center; margin-bottom: 1rem; }
|
|
108
|
-
.minimal-dashboard h1 {
|
|
109
|
-
font-size: 1.75rem;
|
|
110
|
-
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
|
111
|
-
-webkit-background-clip: text;
|
|
112
|
-
-webkit-text-fill-color: transparent;
|
|
113
|
-
background-clip: text;
|
|
114
|
-
}
|
|
115
|
-
.service-groups { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
116
|
-
.service-group {
|
|
117
|
-
background: rgba(255, 255, 255, 0.03);
|
|
118
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
119
|
-
border-radius: 8px;
|
|
120
|
-
overflow: hidden;
|
|
121
|
-
}
|
|
122
|
-
.service-group-summary {
|
|
123
|
-
list-style: none;
|
|
124
|
-
cursor: pointer;
|
|
125
|
-
display: flex;
|
|
126
|
-
align-items: center;
|
|
127
|
-
gap: 0.5rem;
|
|
128
|
-
padding: 0.55rem 0.8rem;
|
|
129
|
-
user-select: none;
|
|
130
|
-
}
|
|
131
|
-
.service-group-summary::-webkit-details-marker { display: none; }
|
|
132
|
-
.service-group-caret {
|
|
133
|
-
font-size: 0.7rem;
|
|
134
|
-
color: #8b5cf6;
|
|
135
|
-
transition: transform 0.15s ease;
|
|
136
|
-
}
|
|
137
|
-
.service-group[open] > .service-group-summary .service-group-caret { transform: rotate(90deg); }
|
|
138
|
-
.service-group-name { font-size: 0.9rem; font-weight: 600; color: #e4e4e7; }
|
|
139
|
-
.service-group-count {
|
|
140
|
-
font-size: 0.7rem;
|
|
141
|
-
color: #a1a1aa;
|
|
142
|
-
background: rgba(255, 255, 255, 0.08);
|
|
143
|
-
border-radius: 999px;
|
|
144
|
-
padding: 0.05rem 0.5rem;
|
|
145
|
-
}
|
|
146
|
-
.service-group .services-list { padding: 0 0.6rem 0.6rem; }
|
|
147
|
-
.services-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
148
|
-
.service-card {
|
|
149
|
-
background: rgba(255, 255, 255, 0.05);
|
|
150
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
151
|
-
border-radius: 8px;
|
|
152
|
-
padding: 0.6rem 0.9rem;
|
|
153
|
-
display: flex;
|
|
154
|
-
align-items: center;
|
|
155
|
-
justify-content: space-between;
|
|
156
|
-
gap: 0.75rem;
|
|
157
|
-
transition: all 0.15s ease;
|
|
158
|
-
}
|
|
159
|
-
.service-card:hover {
|
|
160
|
-
background: rgba(255, 255, 255, 0.08);
|
|
161
|
-
border-color: rgba(139, 92, 246, 0.4);
|
|
162
|
-
}
|
|
163
|
-
.service-info { display: flex; align-items: center; min-width: 0; flex: 1; }
|
|
164
|
-
.service-name {
|
|
165
|
-
font-size: 0.95rem;
|
|
166
|
-
font-weight: 600;
|
|
167
|
-
color: #f4f4f5;
|
|
168
|
-
white-space: nowrap;
|
|
169
|
-
overflow: hidden;
|
|
170
|
-
text-overflow: ellipsis;
|
|
171
|
-
}
|
|
172
|
-
.service-actions { display: flex; flex-shrink: 0; align-items: center; }
|
|
173
|
-
.action-btn {
|
|
174
|
-
background: rgba(255, 255, 255, 0.1);
|
|
175
|
-
color: #e4e4e7;
|
|
176
|
-
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
177
|
-
padding: 0.35rem 0.6rem;
|
|
178
|
-
border-radius: 5px;
|
|
179
|
-
cursor: pointer;
|
|
180
|
-
font-size: 0.75rem;
|
|
181
|
-
font-weight: 500;
|
|
182
|
-
text-decoration: none;
|
|
183
|
-
display: inline-flex;
|
|
184
|
-
align-items: center;
|
|
185
|
-
}
|
|
186
|
-
.action-btn.primary {
|
|
187
|
-
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
|
188
|
-
border: none;
|
|
189
|
-
color: white;
|
|
190
|
-
}
|
|
191
|
-
.action-btn.primary:hover { opacity: 0.9; }
|
|
192
|
-
.empty-state.minimal-empty {
|
|
193
|
-
text-align: center;
|
|
194
|
-
padding: 2rem 1rem;
|
|
195
|
-
background: rgba(255, 255, 255, 0.03);
|
|
196
|
-
border-radius: 10px;
|
|
197
|
-
border: 1px dashed rgba(255, 255, 255, 0.1);
|
|
198
|
-
}
|
|
199
|
-
.empty-state.minimal-empty p { color: #a1a1aa; font-size: 0.95rem; }
|
|
200
|
-
</style>
|
|
201
|
-
</head>
|
|
202
|
-
<body class="minimal-dashboard">
|
|
203
|
-
<div class="container">
|
|
204
|
-
<header>
|
|
205
|
-
<h1>${escapeHtml(proxyName)}</h1>
|
|
206
|
-
</header>
|
|
207
|
-
${servicesHtml}
|
|
208
|
-
</div>
|
|
209
|
-
</body>
|
|
210
|
-
</html>`;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const renderServiceCard = (route) => `
|
|
214
|
-
<div class="service-card" data-path="${escapeHtml(route.path)}">
|
|
215
|
-
<div class="service-info">
|
|
216
|
-
<span class="service-name">${escapeHtml(route.name)}</span>
|
|
217
|
-
<span class="service-path">${escapeHtml(route.path)}</span>
|
|
218
|
-
<span class="service-target">→ ${escapeHtml(route.target)}:${route.port}</span>
|
|
219
|
-
<span class="service-health">${route.healthCheck !== false ? '✅' : '⚪'}</span>
|
|
220
|
-
<span class="service-config"></span>
|
|
221
|
-
</div>
|
|
222
|
-
<div class="service-actions">
|
|
223
|
-
<a href="${escapeHtml(route.path)}" class="action-btn primary" title="Accéder" target="_blank" rel="noopener noreferrer">Ouvrir</a>
|
|
224
|
-
<button onclick="checkHealth('${escapeHtml(route.path)}', this)" class="action-btn" title="Vérifier la santé">Santé</button>
|
|
225
|
-
<button onclick="killPort(${route.port}, this)" class="action-btn danger" title="Tuer le process">Tuer</button>
|
|
226
|
-
</div>
|
|
227
|
-
</div>
|
|
228
|
-
`;
|
|
94
|
+
if (minimalDashboard) return renderMinimalDashboard(routes, proxyName);
|
|
229
95
|
|
|
230
96
|
const servicesHtml = routes.length > 0
|
|
231
|
-
?
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
97
|
+
? `<div class="list" id="servicesList">${routes.map(renderServiceRow).join('')}</div>
|
|
98
|
+
<div class="empty hidden" id="noMatch"><strong>Aucun résultat</strong>Aucun service ne correspond à ce filtre.</div>`
|
|
99
|
+
: `<div class="empty">
|
|
100
|
+
<strong>Aucun service enregistré</strong>
|
|
101
|
+
Ajoutez-en un avec « Nouveau service », ou appelez <code>POST /proxy/register-manual</code>.
|
|
102
|
+
</div>`;
|
|
237
103
|
|
|
238
104
|
return `<!DOCTYPE html>
|
|
239
105
|
<html lang="fr">
|
|
240
106
|
<head>
|
|
241
107
|
<meta charset="UTF-8">
|
|
242
108
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
243
|
-
<title>${escapeHtml(proxyName)}
|
|
109
|
+
<title>${escapeHtml(proxyName)} — Services</title>
|
|
244
110
|
<style>
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
body {
|
|
251
|
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
|
|
252
|
-
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 100%);
|
|
253
|
-
min-height: 100vh;
|
|
254
|
-
color: #e4e4e7;
|
|
255
|
-
padding: 1.5rem;
|
|
256
|
-
}
|
|
257
|
-
.container {
|
|
258
|
-
max-width: 1100px;
|
|
259
|
-
margin: 0 auto;
|
|
260
|
-
}
|
|
261
|
-
header {
|
|
262
|
-
text-align: center;
|
|
263
|
-
margin-bottom: 1.5rem;
|
|
264
|
-
}
|
|
265
|
-
h1 {
|
|
266
|
-
font-size: 2rem;
|
|
267
|
-
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
|
268
|
-
-webkit-background-clip: text;
|
|
269
|
-
-webkit-text-fill-color: transparent;
|
|
270
|
-
background-clip: text;
|
|
271
|
-
margin-bottom: 0.25rem;
|
|
272
|
-
}
|
|
273
|
-
.subtitle {
|
|
274
|
-
color: #a1a1aa;
|
|
275
|
-
font-size: 0.95rem;
|
|
276
|
-
}
|
|
277
|
-
.stats {
|
|
278
|
-
display: flex;
|
|
279
|
-
justify-content: center;
|
|
280
|
-
gap: 1.5rem;
|
|
281
|
-
margin-top: 1rem;
|
|
282
|
-
}
|
|
283
|
-
.stat {
|
|
284
|
-
background: rgba(255, 255, 255, 0.05);
|
|
285
|
-
padding: 0.5rem 1rem;
|
|
286
|
-
border-radius: 8px;
|
|
287
|
-
text-align: center;
|
|
288
|
-
}
|
|
289
|
-
.stat-value {
|
|
290
|
-
font-size: 1.25rem;
|
|
291
|
-
font-weight: bold;
|
|
292
|
-
color: #8b5cf6;
|
|
293
|
-
}
|
|
294
|
-
.stat-label {
|
|
295
|
-
font-size: 0.75rem;
|
|
296
|
-
color: #71717a;
|
|
297
|
-
}
|
|
298
|
-
.toolbar {
|
|
299
|
-
display: flex;
|
|
300
|
-
justify-content: space-between;
|
|
301
|
-
align-items: center;
|
|
302
|
-
margin-bottom: 0.75rem;
|
|
303
|
-
gap: 0.5rem;
|
|
304
|
-
}
|
|
305
|
-
.toolbar-left {
|
|
306
|
-
display: flex;
|
|
307
|
-
gap: 0.5rem;
|
|
308
|
-
align-items: center;
|
|
309
|
-
}
|
|
310
|
-
.toolbar-right {
|
|
311
|
-
display: flex;
|
|
312
|
-
gap: 0.5rem;
|
|
313
|
-
align-items: center;
|
|
314
|
-
}
|
|
315
|
-
.agent-btn {
|
|
316
|
-
background: linear-gradient(90deg, #10b981, #059669);
|
|
317
|
-
color: white;
|
|
318
|
-
border: none;
|
|
319
|
-
padding: 0.5rem 1rem;
|
|
320
|
-
border-radius: 6px;
|
|
321
|
-
font-weight: 600;
|
|
322
|
-
font-size: 0.8rem;
|
|
323
|
-
cursor: pointer;
|
|
324
|
-
transition: opacity 0.2s;
|
|
325
|
-
display: inline-flex;
|
|
326
|
-
align-items: center;
|
|
327
|
-
gap: 0.4rem;
|
|
328
|
-
white-space: nowrap;
|
|
329
|
-
}
|
|
330
|
-
.agent-btn:hover {
|
|
331
|
-
opacity: 0.9;
|
|
332
|
-
}
|
|
333
|
-
.cleanup-btn {
|
|
334
|
-
background: linear-gradient(90deg, #ef4444, #dc2626);
|
|
335
|
-
color: white;
|
|
336
|
-
border: none;
|
|
337
|
-
padding: 0.5rem 1rem;
|
|
338
|
-
border-radius: 6px;
|
|
339
|
-
font-weight: 600;
|
|
340
|
-
font-size: 0.8rem;
|
|
341
|
-
cursor: pointer;
|
|
342
|
-
transition: opacity 0.2s;
|
|
343
|
-
display: inline-flex;
|
|
344
|
-
align-items: center;
|
|
345
|
-
gap: 0.4rem;
|
|
346
|
-
white-space: nowrap;
|
|
347
|
-
}
|
|
348
|
-
.cleanup-btn:hover { opacity: 0.9; }
|
|
349
|
-
.cleanup-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
350
|
-
.add-btn {
|
|
351
|
-
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
|
352
|
-
color: white;
|
|
353
|
-
border: none;
|
|
354
|
-
padding: 0.5rem 1rem;
|
|
355
|
-
border-radius: 6px;
|
|
356
|
-
font-weight: 600;
|
|
357
|
-
font-size: 0.8rem;
|
|
358
|
-
cursor: pointer;
|
|
359
|
-
transition: opacity 0.2s;
|
|
360
|
-
display: inline-flex;
|
|
361
|
-
align-items: center;
|
|
362
|
-
gap: 0.4rem;
|
|
363
|
-
}
|
|
364
|
-
.add-btn:hover {
|
|
365
|
-
opacity: 0.9;
|
|
366
|
-
}
|
|
367
|
-
.modal-overlay {
|
|
368
|
-
display: none;
|
|
369
|
-
position: fixed;
|
|
370
|
-
top: 0;
|
|
371
|
-
left: 0;
|
|
372
|
-
right: 0;
|
|
373
|
-
bottom: 0;
|
|
374
|
-
background: rgba(0, 0, 0, 0.6);
|
|
375
|
-
z-index: 1000;
|
|
376
|
-
align-items: center;
|
|
377
|
-
justify-content: center;
|
|
378
|
-
}
|
|
379
|
-
.modal-overlay.active {
|
|
380
|
-
display: flex;
|
|
381
|
-
}
|
|
382
|
-
.modal {
|
|
383
|
-
background: linear-gradient(135deg, #1e1e3f 0%, #1a1a2e 100%);
|
|
384
|
-
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
385
|
-
border-radius: 12px;
|
|
386
|
-
padding: 1.5rem;
|
|
387
|
-
width: 100%;
|
|
388
|
-
max-width: 360px;
|
|
389
|
-
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.4);
|
|
390
|
-
}
|
|
391
|
-
.modal-header {
|
|
392
|
-
display: flex;
|
|
393
|
-
justify-content: space-between;
|
|
394
|
-
align-items: center;
|
|
395
|
-
margin-bottom: 1rem;
|
|
396
|
-
}
|
|
397
|
-
.modal-header h2 {
|
|
398
|
-
font-size: 1.1rem;
|
|
399
|
-
color: #f4f4f5;
|
|
400
|
-
}
|
|
401
|
-
.modal-close {
|
|
402
|
-
background: transparent;
|
|
403
|
-
border: none;
|
|
404
|
-
color: #71717a;
|
|
405
|
-
font-size: 1.25rem;
|
|
406
|
-
cursor: pointer;
|
|
407
|
-
padding: 0.25rem;
|
|
408
|
-
line-height: 1;
|
|
409
|
-
}
|
|
410
|
-
.modal-close:hover {
|
|
411
|
-
color: #e4e4e7;
|
|
412
|
-
}
|
|
413
|
-
.modal .form-group {
|
|
414
|
-
margin-bottom: 0.75rem;
|
|
415
|
-
}
|
|
416
|
-
.modal .form-group label {
|
|
417
|
-
font-size: 0.75rem;
|
|
418
|
-
color: #a1a1aa;
|
|
419
|
-
margin-bottom: 0.25rem;
|
|
420
|
-
display: block;
|
|
421
|
-
}
|
|
422
|
-
.modal .form-group input {
|
|
423
|
-
background: rgba(0, 0, 0, 0.3);
|
|
424
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
425
|
-
padding: 0.5rem 0.75rem;
|
|
426
|
-
border-radius: 6px;
|
|
427
|
-
color: white;
|
|
428
|
-
font-family: inherit;
|
|
429
|
-
font-size: 0.9rem;
|
|
430
|
-
width: 100%;
|
|
431
|
-
}
|
|
432
|
-
.modal .form-group input:focus {
|
|
433
|
-
outline: none;
|
|
434
|
-
border-color: #8b5cf6;
|
|
435
|
-
background: rgba(0, 0, 0, 0.4);
|
|
436
|
-
}
|
|
437
|
-
.modal .checkbox-group {
|
|
438
|
-
display: flex;
|
|
439
|
-
align-items: center;
|
|
440
|
-
gap: 0.5rem;
|
|
441
|
-
margin-bottom: 1rem;
|
|
442
|
-
}
|
|
443
|
-
.modal .checkbox-group input {
|
|
444
|
-
width: auto;
|
|
445
|
-
}
|
|
446
|
-
.modal .checkbox-group label {
|
|
447
|
-
font-size: 0.75rem;
|
|
448
|
-
color: #a1a1aa;
|
|
449
|
-
margin: 0;
|
|
450
|
-
}
|
|
451
|
-
.modal .submit-btn {
|
|
452
|
-
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
|
453
|
-
color: white;
|
|
454
|
-
border: none;
|
|
455
|
-
padding: 0.6rem 1.25rem;
|
|
456
|
-
border-radius: 6px;
|
|
457
|
-
font-weight: 600;
|
|
458
|
-
font-size: 0.85rem;
|
|
459
|
-
cursor: pointer;
|
|
460
|
-
transition: opacity 0.2s;
|
|
461
|
-
width: 100%;
|
|
462
|
-
}
|
|
463
|
-
.modal .submit-btn:hover {
|
|
464
|
-
opacity: 0.9;
|
|
465
|
-
}
|
|
466
|
-
.service-groups {
|
|
467
|
-
display: flex;
|
|
468
|
-
flex-direction: column;
|
|
469
|
-
gap: 0.6rem;
|
|
470
|
-
}
|
|
471
|
-
.service-group {
|
|
472
|
-
background: rgba(255, 255, 255, 0.03);
|
|
473
|
-
border: 1px solid rgba(255, 255, 255, 0.08);
|
|
474
|
-
border-radius: 10px;
|
|
475
|
-
overflow: hidden;
|
|
476
|
-
}
|
|
477
|
-
.service-group-summary {
|
|
478
|
-
list-style: none;
|
|
479
|
-
cursor: pointer;
|
|
480
|
-
display: flex;
|
|
481
|
-
align-items: center;
|
|
482
|
-
gap: 0.6rem;
|
|
483
|
-
padding: 0.7rem 1rem;
|
|
484
|
-
user-select: none;
|
|
485
|
-
transition: background 0.15s ease;
|
|
486
|
-
}
|
|
487
|
-
.service-group-summary:hover { background: rgba(255, 255, 255, 0.04); }
|
|
488
|
-
.service-group-summary::-webkit-details-marker { display: none; }
|
|
489
|
-
.service-group-caret {
|
|
490
|
-
font-size: 0.75rem;
|
|
491
|
-
color: #8b5cf6;
|
|
492
|
-
transition: transform 0.15s ease;
|
|
493
|
-
}
|
|
494
|
-
.service-group[open] > .service-group-summary .service-group-caret { transform: rotate(90deg); }
|
|
495
|
-
.service-group-name {
|
|
496
|
-
font-size: 0.95rem;
|
|
497
|
-
font-weight: 700;
|
|
498
|
-
color: #f4f4f5;
|
|
499
|
-
flex: 1;
|
|
500
|
-
}
|
|
501
|
-
.service-group-count {
|
|
502
|
-
font-size: 0.72rem;
|
|
503
|
-
font-weight: 600;
|
|
504
|
-
color: #a1a1aa;
|
|
505
|
-
background: rgba(139, 92, 246, 0.15);
|
|
506
|
-
border: 1px solid rgba(139, 92, 246, 0.25);
|
|
507
|
-
border-radius: 999px;
|
|
508
|
-
padding: 0.1rem 0.55rem;
|
|
509
|
-
}
|
|
510
|
-
.service-group .services-list { padding: 0 0.75rem 0.75rem; }
|
|
511
|
-
.services-list {
|
|
512
|
-
display: flex;
|
|
513
|
-
flex-direction: column;
|
|
514
|
-
gap: 0.5rem;
|
|
515
|
-
}
|
|
516
|
-
.service-card {
|
|
517
|
-
background: rgba(255, 255, 255, 0.05);
|
|
518
|
-
border: 1px solid rgba(255, 255, 255, 0.1);
|
|
519
|
-
border-radius: 8px;
|
|
520
|
-
padding: 0.6rem 0.9rem;
|
|
521
|
-
display: flex;
|
|
522
|
-
align-items: center;
|
|
523
|
-
justify-content: space-between;
|
|
524
|
-
gap: 0.75rem;
|
|
525
|
-
transition: all 0.15s ease;
|
|
526
|
-
}
|
|
527
|
-
.service-card:hover {
|
|
528
|
-
background: rgba(255, 255, 255, 0.08);
|
|
529
|
-
border-color: rgba(139, 92, 246, 0.4);
|
|
530
|
-
}
|
|
531
|
-
.service-info {
|
|
532
|
-
display: flex;
|
|
533
|
-
align-items: center;
|
|
534
|
-
gap: 0.6rem;
|
|
535
|
-
flex-wrap: wrap;
|
|
536
|
-
min-width: 0;
|
|
537
|
-
flex: 1;
|
|
538
|
-
}
|
|
539
|
-
.service-name {
|
|
540
|
-
font-size: 0.95rem;
|
|
541
|
-
font-weight: 600;
|
|
542
|
-
color: #f4f4f5;
|
|
543
|
-
white-space: nowrap;
|
|
544
|
-
}
|
|
545
|
-
.service-path {
|
|
546
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
547
|
-
font-size: 0.75rem;
|
|
548
|
-
color: #8b5cf6;
|
|
549
|
-
background: rgba(139, 92, 246, 0.15);
|
|
550
|
-
padding: 0.15rem 0.5rem;
|
|
551
|
-
border-radius: 4px;
|
|
552
|
-
white-space: nowrap;
|
|
553
|
-
}
|
|
554
|
-
.service-target {
|
|
555
|
-
font-size: 0.75rem;
|
|
556
|
-
color: #71717a;
|
|
557
|
-
white-space: nowrap;
|
|
558
|
-
}
|
|
559
|
-
.service-health {
|
|
560
|
-
font-size: 0.7rem;
|
|
561
|
-
}
|
|
562
|
-
.service-config {
|
|
563
|
-
font-size: 0.7rem;
|
|
564
|
-
color: #10b981;
|
|
565
|
-
background: rgba(16, 185, 129, 0.1);
|
|
566
|
-
border: 1px solid rgba(16, 185, 129, 0.25);
|
|
567
|
-
padding: 0.1rem 0.45rem;
|
|
568
|
-
border-radius: 4px;
|
|
569
|
-
white-space: nowrap;
|
|
570
|
-
}
|
|
571
|
-
.service-config:empty { display: none; }
|
|
572
|
-
.service-actions {
|
|
573
|
-
display: flex;
|
|
574
|
-
gap: 0.4rem;
|
|
575
|
-
flex-shrink: 0;
|
|
576
|
-
align-items: center;
|
|
577
|
-
}
|
|
578
|
-
.action-btn {
|
|
579
|
-
background: rgba(255, 255, 255, 0.1);
|
|
580
|
-
color: #e4e4e7;
|
|
581
|
-
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
582
|
-
padding: 0.35rem 0.6rem;
|
|
583
|
-
border-radius: 5px;
|
|
584
|
-
cursor: pointer;
|
|
585
|
-
font-size: 0.75rem;
|
|
586
|
-
font-weight: 500;
|
|
587
|
-
transition: all 0.15s;
|
|
588
|
-
text-decoration: none;
|
|
589
|
-
display: inline-flex;
|
|
590
|
-
align-items: center;
|
|
591
|
-
}
|
|
592
|
-
.action-btn:hover {
|
|
593
|
-
background: rgba(255, 255, 255, 0.15);
|
|
594
|
-
border-color: rgba(255, 255, 255, 0.25);
|
|
595
|
-
}
|
|
596
|
-
.action-btn.primary {
|
|
597
|
-
background: linear-gradient(90deg, #6366f1, #8b5cf6);
|
|
598
|
-
border: none;
|
|
599
|
-
color: white;
|
|
600
|
-
}
|
|
601
|
-
.action-btn.primary:hover {
|
|
602
|
-
opacity: 0.9;
|
|
603
|
-
}
|
|
604
|
-
.action-btn.loading {
|
|
605
|
-
opacity: 0.5;
|
|
606
|
-
cursor: not-allowed;
|
|
607
|
-
}
|
|
608
|
-
.action-btn.healthy {
|
|
609
|
-
background: rgba(16, 185, 129, 0.2);
|
|
610
|
-
border-color: rgba(16, 185, 129, 0.4);
|
|
611
|
-
color: #10b981;
|
|
612
|
-
}
|
|
613
|
-
.action-btn.unhealthy {
|
|
614
|
-
background: rgba(239, 68, 68, 0.2);
|
|
615
|
-
border-color: rgba(239, 68, 68, 0.4);
|
|
616
|
-
color: #ef4444;
|
|
617
|
-
}
|
|
618
|
-
.action-btn.danger {
|
|
619
|
-
background: rgba(239, 68, 68, 0.15);
|
|
620
|
-
border-color: rgba(239, 68, 68, 0.35);
|
|
621
|
-
color: #ef4444;
|
|
622
|
-
}
|
|
623
|
-
.action-btn.danger:hover {
|
|
624
|
-
background: rgba(239, 68, 68, 0.25);
|
|
625
|
-
border-color: rgba(239, 68, 68, 0.5);
|
|
626
|
-
}
|
|
627
|
-
.empty-state {
|
|
628
|
-
text-align: center;
|
|
629
|
-
padding: 2.5rem 1.5rem;
|
|
630
|
-
background: rgba(255, 255, 255, 0.03);
|
|
631
|
-
border-radius: 10px;
|
|
632
|
-
border: 1px dashed rgba(255, 255, 255, 0.1);
|
|
633
|
-
}
|
|
634
|
-
.empty-state h2 {
|
|
635
|
-
color: #a1a1aa;
|
|
636
|
-
font-size: 1rem;
|
|
637
|
-
margin-bottom: 0.5rem;
|
|
638
|
-
}
|
|
639
|
-
.empty-state p {
|
|
640
|
-
color: #71717a;
|
|
641
|
-
font-size: 0.85rem;
|
|
642
|
-
margin-bottom: 0.25rem;
|
|
643
|
-
}
|
|
644
|
-
.code {
|
|
645
|
-
font-family: 'SF Mono', 'Fira Code', monospace;
|
|
646
|
-
background: rgba(0, 0, 0, 0.3);
|
|
647
|
-
padding: 0.15rem 0.4rem;
|
|
648
|
-
border-radius: 4px;
|
|
649
|
-
font-size: 0.8rem;
|
|
650
|
-
}
|
|
651
|
-
footer {
|
|
652
|
-
margin-top: 1.5rem;
|
|
653
|
-
text-align: center;
|
|
654
|
-
color: #52525b;
|
|
655
|
-
font-size: 0.75rem;
|
|
656
|
-
}
|
|
657
|
-
footer a {
|
|
658
|
-
color: #8b5cf6;
|
|
659
|
-
text-decoration: none;
|
|
660
|
-
}
|
|
661
|
-
footer a:hover {
|
|
662
|
-
text-decoration: underline;
|
|
663
|
-
}
|
|
664
|
-
@media (max-width: 600px) {
|
|
665
|
-
.toolbar {
|
|
666
|
-
flex-direction: column;
|
|
667
|
-
}
|
|
668
|
-
.toolbar-right {
|
|
669
|
-
width: 100%;
|
|
670
|
-
justify-content: flex-end;
|
|
671
|
-
}
|
|
672
|
-
.service-card {
|
|
673
|
-
flex-direction: column;
|
|
674
|
-
align-items: flex-start;
|
|
675
|
-
gap: 0.5rem;
|
|
676
|
-
}
|
|
677
|
-
.service-actions {
|
|
678
|
-
width: 100%;
|
|
679
|
-
justify-content: flex-end;
|
|
680
|
-
}
|
|
681
|
-
.modal {
|
|
682
|
-
margin: 1rem;
|
|
683
|
-
max-width: none;
|
|
684
|
-
}
|
|
685
|
-
}
|
|
111
|
+
${BASE_CSS}
|
|
112
|
+
${SHELL_CSS}
|
|
113
|
+
.row-config { max-width: 180px; overflow: hidden; text-overflow: ellipsis; }
|
|
114
|
+
.row-status { margin-left: 8px; color: var(--text-3); font-family: var(--sans); }
|
|
686
115
|
</style>
|
|
687
116
|
</head>
|
|
688
117
|
<body>
|
|
689
|
-
<div class="
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
118
|
+
<div class="app">
|
|
119
|
+
${renderSidebar({
|
|
120
|
+
proxyName,
|
|
121
|
+
active: 'services',
|
|
122
|
+
serviceCount: routes.length,
|
|
123
|
+
hasCleanUp: !!cleanUpCommand,
|
|
124
|
+
hasAuth,
|
|
125
|
+
sectionLabel: routes.length ? 'GROUPES' : '',
|
|
126
|
+
sectionHtml: renderGroupFilters(routes),
|
|
127
|
+
statusDetail: `${availablePorts} ports libres · ${routes.length} service${routes.length > 1 ? 's' : ''}`
|
|
128
|
+
})}
|
|
129
|
+
|
|
130
|
+
<main class="main">
|
|
131
|
+
<div class="main-inner">
|
|
132
|
+
<header class="page-head">
|
|
133
|
+
<div>
|
|
134
|
+
<h1 class="page-title">Services</h1>
|
|
135
|
+
<p class="page-sub">${routes.length} service${routes.length > 1 ? 's' : ''} exposé${routes.length > 1 ? 's' : ''} · ${availablePorts} ports disponibles</p>
|
|
136
|
+
</div>
|
|
137
|
+
<div class="page-actions">
|
|
138
|
+
<div class="search">
|
|
139
|
+
${icon('search')}
|
|
140
|
+
<input class="input" type="search" id="searchInput" placeholder="Rechercher un service…" oninput="applyFilters()" autocomplete="off" style="width:240px;">
|
|
141
|
+
</div>
|
|
142
|
+
<button class="btn btn-primary" onclick="openModal()">${icon('plus')}<span>Nouveau service</span></button>
|
|
143
|
+
</div>
|
|
144
|
+
</header>
|
|
145
|
+
|
|
146
|
+
<div class="list-head">
|
|
147
|
+
<span id="listTitle">Vos services</span>
|
|
148
|
+
<button class="icon-btn" id="refreshConfigsBtn" onclick="loadActiveConfigs()" title="Rafraîchir les configurations actives">${icon('refresh', 14)}</button>
|
|
149
|
+
<span class="spacer"></span>
|
|
150
|
+
<span class="list-head-meta" id="listCount"></span>
|
|
701
151
|
</div>
|
|
702
|
-
</div>
|
|
703
|
-
</header>
|
|
704
152
|
|
|
705
|
-
|
|
706
|
-
<div class="toolbar-left">
|
|
707
|
-
<button class="agent-btn" onclick="openAgent()">▶ Lancer Agent</button>
|
|
708
|
-
${cleanUpCommand ? '<button class="cleanup-btn" id="cleanupBtn" onclick="runCleanUp()">🗑 Nettoyer les process</button>' : ''}
|
|
153
|
+
${servicesHtml}
|
|
709
154
|
</div>
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
<button class="action-btn" id="refreshConfigsBtn" onclick="loadActiveConfigs()">⟳ Configs</button>
|
|
713
|
-
<button class="add-btn" onclick="openModal()">+ Ajouter</button>
|
|
714
|
-
${hasAuth ? '<button class="action-btn" onclick="logout()">Déconnexion</button>' : ''}
|
|
715
|
-
</div>
|
|
716
|
-
</div>
|
|
717
|
-
|
|
718
|
-
${servicesHtml}
|
|
155
|
+
</main>
|
|
156
|
+
</div>
|
|
719
157
|
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
</div>
|
|
726
|
-
<form id="registerForm">
|
|
727
|
-
<div class="form-group">
|
|
728
|
-
<label for="path">Chemin</label>
|
|
729
|
-
<input type="text" id="path" name="path" placeholder="/mon-app" required>
|
|
730
|
-
</div>
|
|
731
|
-
<div class="form-group">
|
|
732
|
-
<label for="name">Nom</label>
|
|
733
|
-
<input type="text" id="name" name="name" placeholder="Mon Application">
|
|
734
|
-
</div>
|
|
735
|
-
<div class="form-group">
|
|
736
|
-
<label for="target">Hôte cible</label>
|
|
737
|
-
<input type="text" id="target" name="target" placeholder="http://localhost" value="http://localhost">
|
|
738
|
-
</div>
|
|
739
|
-
<div class="form-group">
|
|
740
|
-
<label for="port">Port</label>
|
|
741
|
-
<input type="number" id="port" name="port" placeholder="4000" required>
|
|
742
|
-
</div>
|
|
743
|
-
<div class="form-group">
|
|
744
|
-
<label for="group">Groupe (optionnel)</label>
|
|
745
|
-
<input type="text" id="group" name="group" placeholder="Laisser vide pour « Non groupées »">
|
|
746
|
-
</div>
|
|
747
|
-
<div class="checkbox-group">
|
|
748
|
-
<input type="checkbox" id="healthCheck" name="healthCheck">
|
|
749
|
-
<label for="healthCheck">Health check actif</label>
|
|
750
|
-
</div>
|
|
751
|
-
<button type="submit" class="submit-btn" id="submitBtn">Enregistrer</button>
|
|
752
|
-
</form>
|
|
158
|
+
<div class="overlay" id="modalOverlay" onclick="closeModalOnOverlay(event)">
|
|
159
|
+
<div class="modal">
|
|
160
|
+
<div class="modal-head">
|
|
161
|
+
<h2>Nouveau service</h2>
|
|
162
|
+
<button class="icon-btn" onclick="closeModal()" aria-label="Fermer">${icon('x')}</button>
|
|
753
163
|
</div>
|
|
164
|
+
<form id="registerForm">
|
|
165
|
+
<div class="field">
|
|
166
|
+
<label for="path">Chemin</label>
|
|
167
|
+
<input class="input" type="text" id="path" name="path" placeholder="/mon-app" required>
|
|
168
|
+
</div>
|
|
169
|
+
<div class="field">
|
|
170
|
+
<label for="name">Nom</label>
|
|
171
|
+
<input class="input" type="text" id="name" name="name" placeholder="Mon Application">
|
|
172
|
+
</div>
|
|
173
|
+
<div class="field">
|
|
174
|
+
<label for="target">Hôte cible</label>
|
|
175
|
+
<input class="input" type="text" id="target" name="target" placeholder="http://localhost" value="http://localhost">
|
|
176
|
+
</div>
|
|
177
|
+
<div class="field">
|
|
178
|
+
<label for="port">Port</label>
|
|
179
|
+
<input class="input" type="number" id="port" name="port" placeholder="4000" required>
|
|
180
|
+
</div>
|
|
181
|
+
<div class="field">
|
|
182
|
+
<label for="group">Groupe (optionnel)</label>
|
|
183
|
+
<input class="input" type="text" id="group" name="group" placeholder="Vide → « ${UNGROUPED_LABEL} »">
|
|
184
|
+
</div>
|
|
185
|
+
<div class="field-inline">
|
|
186
|
+
<input type="checkbox" id="healthCheck" name="healthCheck">
|
|
187
|
+
<label for="healthCheck">Health check actif</label>
|
|
188
|
+
</div>
|
|
189
|
+
<button type="submit" class="btn btn-primary" id="submitBtn" style="width:100%;">Enregistrer</button>
|
|
190
|
+
</form>
|
|
754
191
|
</div>
|
|
755
|
-
|
|
756
|
-
<footer>
|
|
757
|
-
<p>API: <a href="/proxy/routes">/proxy/routes</a> | <a href="/proxy/health">/proxy/health</a></p>
|
|
758
|
-
</footer>
|
|
759
192
|
</div>
|
|
760
193
|
|
|
761
194
|
<script>
|
|
195
|
+
${SHELL_JS}
|
|
196
|
+
|
|
197
|
+
document.querySelector('.brand-host').textContent = location.host;
|
|
198
|
+
refreshSyncBadge();
|
|
199
|
+
|
|
200
|
+
// --- Filtres : groupe (panneau) + recherche, combinés ---
|
|
201
|
+
let currentGroup = '';
|
|
202
|
+
|
|
203
|
+
function setGroupFilter(btn) {
|
|
204
|
+
currentGroup = btn.dataset.filter || '';
|
|
205
|
+
document.querySelectorAll('[data-filter]').forEach(el => el.classList.toggle('active', el === btn));
|
|
206
|
+
document.getElementById('listTitle').textContent = currentGroup || 'Vos services';
|
|
207
|
+
applyFilters();
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function applyFilters() {
|
|
211
|
+
const input = document.getElementById('searchInput');
|
|
212
|
+
const query = input ? input.value.trim().toLowerCase() : '';
|
|
213
|
+
const rows = document.querySelectorAll('#servicesList .row');
|
|
214
|
+
let visible = 0;
|
|
215
|
+
|
|
216
|
+
rows.forEach(row => {
|
|
217
|
+
const matchGroup = !currentGroup || row.dataset.group === currentGroup;
|
|
218
|
+
const matchQuery = !query || row.dataset.search.includes(query);
|
|
219
|
+
const show = matchGroup && matchQuery;
|
|
220
|
+
row.classList.toggle('hidden', !show);
|
|
221
|
+
if (show) visible++;
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
const noMatch = document.getElementById('noMatch');
|
|
225
|
+
if (noMatch) noMatch.classList.toggle('hidden', visible > 0 || rows.length === 0);
|
|
226
|
+
const count = document.getElementById('listCount');
|
|
227
|
+
if (count) count.textContent = rows.length ? visible + ' sur ' + rows.length : '';
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
applyFilters();
|
|
231
|
+
|
|
232
|
+
// --- Configurations actives, affichées en étiquette sur chaque ligne ---
|
|
762
233
|
async function loadActiveConfigs() {
|
|
763
234
|
const btn = document.getElementById('refreshConfigsBtn');
|
|
764
|
-
if (btn) { btn.disabled = true; btn.
|
|
235
|
+
if (btn) { btn.disabled = true; btn.classList.add('spinning'); }
|
|
765
236
|
try {
|
|
766
237
|
const res = await fetch('/proxy/mcp-configurations');
|
|
767
238
|
const data = await res.json();
|
|
768
239
|
(data.results || []).forEach(mcp => {
|
|
769
|
-
const
|
|
770
|
-
if (!
|
|
771
|
-
const
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
240
|
+
const row = document.querySelector('.row[data-path="' + mcp.path + '"]');
|
|
241
|
+
if (!row) return;
|
|
242
|
+
const tag = row.querySelector('.row-config');
|
|
243
|
+
const active = mcp.configurations && mcp.configurations.find(c => c.active);
|
|
244
|
+
if (active) {
|
|
245
|
+
tag.textContent = active.name || active.id;
|
|
246
|
+
tag.classList.remove('hidden');
|
|
247
|
+
} else {
|
|
248
|
+
tag.textContent = '';
|
|
249
|
+
tag.classList.add('hidden');
|
|
250
|
+
}
|
|
775
251
|
});
|
|
776
252
|
} catch (_) {} finally {
|
|
777
|
-
if (btn) { btn.disabled = false; btn.
|
|
253
|
+
if (btn) { btn.disabled = false; btn.classList.remove('spinning'); }
|
|
778
254
|
}
|
|
779
255
|
}
|
|
780
256
|
loadActiveConfigs();
|
|
781
257
|
|
|
258
|
+
// --- Actions d'une ligne ---
|
|
259
|
+
function rowOf(el) { return el.closest('.row'); }
|
|
260
|
+
|
|
261
|
+
/** Message éphémère à droite du chemin, plutôt qu'un bouton qui change de texte. */
|
|
262
|
+
function setRowStatus(row, text, color) {
|
|
263
|
+
let el = row.querySelector('.row-status');
|
|
264
|
+
if (!el) {
|
|
265
|
+
el = document.createElement('span');
|
|
266
|
+
el.className = 'row-status';
|
|
267
|
+
row.querySelector('.row-meta').appendChild(el);
|
|
268
|
+
}
|
|
269
|
+
el.textContent = text ? ' · ' + text : '';
|
|
270
|
+
el.style.color = color || 'var(--text-3)';
|
|
271
|
+
if (text) setTimeout(() => { if (el.textContent === ' · ' + text) el.textContent = ''; }, 4000);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
async function checkHealth(el) {
|
|
275
|
+
closeAllMenus();
|
|
276
|
+
const row = rowOf(el);
|
|
277
|
+
const dot = row.querySelector('.dot');
|
|
278
|
+
setRowStatus(row, 'vérification…');
|
|
279
|
+
try {
|
|
280
|
+
const res = await fetch('/proxy/check', {
|
|
281
|
+
method: 'POST',
|
|
282
|
+
headers: { 'Content-Type': 'application/json' },
|
|
283
|
+
body: JSON.stringify({ path: row.dataset.path })
|
|
284
|
+
});
|
|
285
|
+
const data = await res.json();
|
|
286
|
+
dot.className = 'dot ' + (data.healthy ? 'up' : 'down');
|
|
287
|
+
setRowStatus(row, data.healthy ? 'en ligne' : 'hors ligne', data.healthy ? 'var(--green)' : 'var(--red)');
|
|
288
|
+
if (!data.healthy) setTimeout(() => window.location.reload(), 2500);
|
|
289
|
+
} catch (_) {
|
|
290
|
+
setRowStatus(row, 'erreur réseau', 'var(--red)');
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function copyUrl(el) {
|
|
295
|
+
closeAllMenus();
|
|
296
|
+
const row = rowOf(el);
|
|
297
|
+
const url = location.origin + row.dataset.path;
|
|
298
|
+
navigator.clipboard.writeText(url)
|
|
299
|
+
.then(() => setRowStatus(row, 'URL copiée', 'var(--green)'))
|
|
300
|
+
.catch(() => setRowStatus(row, 'copie impossible', 'var(--red)'));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async function killPort(el) {
|
|
304
|
+
closeAllMenus();
|
|
305
|
+
const row = rowOf(el);
|
|
306
|
+
const port = row.dataset.port;
|
|
307
|
+
if (!confirm('Tuer le process sur le port ' + port + ' ?')) return;
|
|
308
|
+
setRowStatus(row, 'arrêt…');
|
|
309
|
+
try {
|
|
310
|
+
await fetch('/proxy/kill-port', {
|
|
311
|
+
method: 'POST',
|
|
312
|
+
headers: { 'Content-Type': 'application/json' },
|
|
313
|
+
body: JSON.stringify({ port: parseInt(port, 10) })
|
|
314
|
+
});
|
|
315
|
+
row.querySelector('.dot').className = 'dot down';
|
|
316
|
+
setRowStatus(row, 'process tué', 'var(--amber)');
|
|
317
|
+
} catch (_) {
|
|
318
|
+
setRowStatus(row, 'échec', 'var(--red)');
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// --- Modale d'ajout ---
|
|
782
323
|
function openModal() {
|
|
783
324
|
document.getElementById('modalOverlay').classList.add('active');
|
|
784
325
|
document.getElementById('path').focus();
|
|
@@ -791,19 +332,22 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
|
|
|
791
332
|
}
|
|
792
333
|
|
|
793
334
|
function closeModalOnOverlay(event) {
|
|
794
|
-
if (event.target === document.getElementById('modalOverlay'))
|
|
795
|
-
closeModal();
|
|
796
|
-
}
|
|
335
|
+
if (event.target === document.getElementById('modalOverlay')) closeModal();
|
|
797
336
|
}
|
|
798
337
|
|
|
799
338
|
document.addEventListener('keydown', (e) => {
|
|
800
339
|
if (e.key === 'Escape') closeModal();
|
|
340
|
+
if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
|
|
341
|
+
e.preventDefault();
|
|
342
|
+
const input = document.getElementById('searchInput');
|
|
343
|
+
if (input) input.focus();
|
|
344
|
+
}
|
|
801
345
|
});
|
|
802
346
|
|
|
803
347
|
document.getElementById('registerForm').addEventListener('submit', async (e) => {
|
|
804
348
|
e.preventDefault();
|
|
805
349
|
const btn = document.getElementById('submitBtn');
|
|
806
|
-
const originalText = btn.
|
|
350
|
+
const originalText = btn.textContent;
|
|
807
351
|
|
|
808
352
|
const formData = {
|
|
809
353
|
path: document.getElementById('path').value,
|
|
@@ -814,7 +358,7 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
|
|
|
814
358
|
healthCheck: document.getElementById('healthCheck').checked
|
|
815
359
|
};
|
|
816
360
|
|
|
817
|
-
btn.
|
|
361
|
+
btn.textContent = 'Enregistrement…';
|
|
818
362
|
btn.disabled = true;
|
|
819
363
|
|
|
820
364
|
try {
|
|
@@ -823,115 +367,77 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
|
|
|
823
367
|
headers: { 'Content-Type': 'application/json' },
|
|
824
368
|
body: JSON.stringify(formData)
|
|
825
369
|
});
|
|
826
|
-
|
|
827
370
|
const data = await response.json();
|
|
828
371
|
|
|
829
372
|
if (response.ok) {
|
|
830
|
-
btn.
|
|
831
|
-
setTimeout(() =>
|
|
832
|
-
window.location.reload();
|
|
833
|
-
}, 1000);
|
|
373
|
+
btn.textContent = 'Enregistré';
|
|
374
|
+
setTimeout(() => window.location.reload(), 800);
|
|
834
375
|
} else {
|
|
835
376
|
alert('Erreur: ' + data.error);
|
|
836
|
-
btn.
|
|
377
|
+
btn.textContent = originalText;
|
|
837
378
|
btn.disabled = false;
|
|
838
379
|
}
|
|
839
380
|
} catch (error) {
|
|
840
|
-
alert('Erreur
|
|
841
|
-
btn.
|
|
381
|
+
alert('Erreur réseau');
|
|
382
|
+
btn.textContent = originalText;
|
|
842
383
|
btn.disabled = false;
|
|
843
384
|
}
|
|
844
385
|
});
|
|
386
|
+
</script>
|
|
387
|
+
</body>
|
|
388
|
+
</html>`;
|
|
389
|
+
}
|
|
845
390
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
async function runCleanUp() {
|
|
851
|
-
if (!confirm('Nettoyer tous les processus ?')) return;
|
|
852
|
-
const btn = document.getElementById('cleanupBtn');
|
|
853
|
-
const originalText = btn.innerHTML;
|
|
854
|
-
btn.disabled = true;
|
|
855
|
-
btn.innerHTML = '⌛ Nettoyage\u2026';
|
|
856
|
-
try {
|
|
857
|
-
await fetch('/proxy/cleanup', { method: 'POST' });
|
|
858
|
-
btn.innerHTML = '✓ Nettoy\u00e9';
|
|
859
|
-
setTimeout(() => { btn.innerHTML = originalText; btn.disabled = false; }, 3000);
|
|
860
|
-
} catch (_) {
|
|
861
|
-
btn.innerHTML = originalText;
|
|
862
|
-
btn.disabled = false;
|
|
863
|
-
}
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
function logout() {
|
|
867
|
-
const form = document.createElement('form');
|
|
868
|
-
form.method = 'POST';
|
|
869
|
-
form.action = '/proxy/logout';
|
|
870
|
-
document.body.appendChild(form);
|
|
871
|
-
form.submit();
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
async function killPort(port, btn) {
|
|
875
|
-
if (btn.classList.contains('loading')) return;
|
|
876
|
-
if (!confirm('Tuer le process sur le port ' + port + ' ?')) return;
|
|
877
|
-
const originalText = btn.innerText;
|
|
878
|
-
btn.innerText = '\u2026';
|
|
879
|
-
btn.classList.add('loading');
|
|
880
|
-
try {
|
|
881
|
-
await fetch('/proxy/kill-port', {
|
|
882
|
-
method: 'POST',
|
|
883
|
-
headers: { 'Content-Type': 'application/json' },
|
|
884
|
-
body: JSON.stringify({ port })
|
|
885
|
-
});
|
|
886
|
-
btn.innerText = '\u2713 Tu\u00e9';
|
|
887
|
-
setTimeout(() => { btn.innerText = originalText; btn.classList.remove('loading'); }, 2000);
|
|
888
|
-
} catch (_) {
|
|
889
|
-
btn.innerText = originalText;
|
|
890
|
-
btn.classList.remove('loading');
|
|
891
|
-
}
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
async function checkHealth(path, btn) {
|
|
895
|
-
if (btn.classList.contains('loading')) return;
|
|
896
|
-
|
|
897
|
-
const originalText = btn.innerText;
|
|
898
|
-
btn.innerText = 'V\u00e9rification...';
|
|
899
|
-
btn.classList.add('loading');
|
|
900
|
-
|
|
901
|
-
try {
|
|
902
|
-
const response = await fetch('/proxy/check', {
|
|
903
|
-
method: 'POST',
|
|
904
|
-
headers: { 'Content-Type': 'application/json' },
|
|
905
|
-
body: JSON.stringify({ path })
|
|
906
|
-
});
|
|
391
|
+
/** Variante réduite : nom et bouton d'ouverture, groupes affichés comme intertitres. */
|
|
392
|
+
function renderMinimalDashboard(routes, proxyName) {
|
|
393
|
+
const groups = groupRoutes(routes);
|
|
907
394
|
|
|
908
|
-
|
|
395
|
+
const servicesHtml = routes.length > 0
|
|
396
|
+
? groups.map(group => `
|
|
397
|
+
<section class="min-group">
|
|
398
|
+
${groups.length > 1 ? `<div class="min-group-label">${escapeHtml(group.name)}</div>` : ''}
|
|
399
|
+
<div class="list">
|
|
400
|
+
${group.routes.map(route => `
|
|
401
|
+
<div class="row">
|
|
402
|
+
<div class="row-main"><div class="row-name">${escapeHtml(route.name)}</div></div>
|
|
403
|
+
<a class="row-open" href="${escapeHtml(route.path)}" target="_blank" rel="noopener noreferrer">Ouvrir</a>
|
|
404
|
+
</div>`).join('')}
|
|
405
|
+
</div>
|
|
406
|
+
</section>`).join('')
|
|
407
|
+
: '<div class="empty"><strong>Aucun service</strong>Rien n\'est enregistré pour le moment.</div>';
|
|
909
408
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
409
|
+
return `<!DOCTYPE html>
|
|
410
|
+
<html lang="fr">
|
|
411
|
+
<head>
|
|
412
|
+
<meta charset="UTF-8">
|
|
413
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
414
|
+
<title>${escapeHtml(proxyName)}</title>
|
|
415
|
+
<style>
|
|
416
|
+
${BASE_CSS}
|
|
417
|
+
body { padding: 48px 24px; }
|
|
418
|
+
.min { max-width: 560px; margin: 0 auto; }
|
|
419
|
+
.min-head { display: flex; align-items: center; gap: 12px; margin-bottom: 32px; }
|
|
420
|
+
.min-logo {
|
|
421
|
+
width: 32px; height: 32px; border-radius: 9px; background: var(--accent);
|
|
422
|
+
display: grid; place-items: center; color: #fff;
|
|
423
|
+
}
|
|
424
|
+
.min-head h1 { font-size: 18px; font-weight: 600; }
|
|
425
|
+
.min-group + .min-group { margin-top: 28px; }
|
|
426
|
+
.min-group-label {
|
|
427
|
+
font-size: 10px; font-weight: 600; letter-spacing: .09em;
|
|
428
|
+
color: var(--text-4); margin-bottom: 8px;
|
|
429
|
+
}
|
|
430
|
+
.min .row { min-height: 52px; }
|
|
431
|
+
</style>
|
|
432
|
+
</head>
|
|
433
|
+
<body>
|
|
434
|
+
<main class="min">
|
|
435
|
+
<header class="min-head">
|
|
436
|
+
<span class="min-logo">${icon('network', 16)}</span>
|
|
437
|
+
<h1>${escapeHtml(proxyName)}</h1>
|
|
438
|
+
</header>
|
|
439
|
+
${servicesHtml}
|
|
440
|
+
</main>
|
|
935
441
|
</body>
|
|
936
442
|
</html>`;
|
|
937
443
|
}
|