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