@imenam/self-register-proxy 1.0.40 → 1.0.42

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 CHANGED
@@ -82,7 +82,8 @@ const response = await fetch('http://localhost:3000/proxy/register', {
82
82
  headers: { 'Content-Type': 'application/json' },
83
83
  body: JSON.stringify({
84
84
  path: '/myapp', // Chemin d'accès via le proxy
85
- name: 'My App' // Nom de l'application (optionnel)
85
+ name: 'My App', // Nom de l'application (optionnel)
86
+ group: 'Google' // Section repliable du dashboard (optionnel)
86
87
  })
87
88
  });
88
89
 
@@ -163,6 +164,7 @@ Enregistre une nouvelle route et attribue un port.
163
164
  | `path` | string | ✅ | Chemin URL pour accéder à l'app |
164
165
  | `name` | string | ❌ | Nom descriptif |
165
166
  | `port` | number | ❌ | Port spécifique (sinon auto-attribué) |
167
+ | `group` | string | ❌ | Section repliable du dashboard (repliée par défaut). Vide/absent ⇒ section « Non groupées » |
166
168
 
167
169
  **Response (201):**
168
170
  ```json
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@imenam/self-register-proxy",
3
- "version": "1.0.40",
3
+ "version": "1.0.42",
4
4
  "description": "Dynamic reverse proxy with self-registration API - applications can register themselves and receive an automatically assigned port",
5
5
  "main": "proxy-client.js",
6
6
  "bin": {
@@ -77,6 +77,7 @@ router.get('/', requireAuth, (req, res) => {
77
77
  target: value.target || 'http://localhost',
78
78
  name: value.name,
79
79
  healthCheck: value.healthCheck,
80
+ group: value.group || null,
80
81
  registeredAt: value.registeredAt,
81
82
  fullTarget: `${value.target || 'http://localhost'}:${value.port}`
82
83
  });
@@ -10,8 +10,18 @@ const router = Router();
10
10
  /**
11
11
  * Logique commune d'enregistrement (auto et manuel)
12
12
  */
13
- async function registerRouteInternal({ path, name, port, target, healthCheck, autoAssignPort }) {
13
+ /**
14
+ * Normalise un nom de groupe : trim, et chaîne vide => undefined (non groupé).
15
+ */
16
+ function normalizeGroup(group) {
17
+ if (typeof group !== 'string') return undefined;
18
+ const trimmed = group.trim();
19
+ return trimmed === '' ? undefined : trimmed;
20
+ }
21
+
22
+ async function registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort }) {
14
23
  const normalizedPath = path.startsWith('/') ? path : `/${path}`;
24
+ const normalizedGroup = normalizeGroup(group);
15
25
 
16
26
  let finalTarget = target || 'http://localhost';
17
27
  if (!finalTarget.startsWith('http://') && !finalTarget.startsWith('https://')) {
@@ -63,10 +73,11 @@ async function registerRouteInternal({ path, name, port, target, healthCheck, au
63
73
  target: finalTarget,
64
74
  name: name || normalizedPath,
65
75
  healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
76
+ group: normalizedGroup,
66
77
  registeredAt: new Date().toISOString()
67
78
  });
68
79
 
69
- console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]`);
80
+ console.log(`[REGISTER] ${normalizedPath} -> ${finalTarget}:${assignedPort} (${name || 'unnamed'}) [Health: ${healthCheck !== undefined ? healthCheck : 'default'}]${normalizedGroup ? ` [Group: ${normalizedGroup}]` : ''}`);
70
81
 
71
82
  return {
72
83
  success: true,
@@ -75,6 +86,7 @@ async function registerRouteInternal({ path, name, port, target, healthCheck, au
75
86
  target: finalTarget,
76
87
  name: name || normalizedPath,
77
88
  healthCheck: healthCheck !== undefined ? healthCheck === true || healthCheck === 'true' : true,
89
+ group: normalizedGroup,
78
90
  message: `Route registered. Target: ${finalTarget}:${assignedPort}`
79
91
  };
80
92
 
@@ -95,12 +107,12 @@ async function registerRouteInternal({ path, name, port, target, healthCheck, au
95
107
  * Enregistrement automatique (rétro-compatibilité)
96
108
  */
97
109
  router.post('/proxy/register', requireLocalOnly, async (req, res) => {
98
- const { path, name, target, healthCheck } = req.body;
110
+ const { path, name, target, healthCheck, group } = req.body;
99
111
 
100
112
  if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
101
113
 
102
114
  try {
103
- const result = await registerRouteInternal({ path, name, target, healthCheck, autoAssignPort: true });
115
+ const result = await registerRouteInternal({ path, name, target, healthCheck, group, autoAssignPort: true });
104
116
  res.json(result);
105
117
  } catch (error) {
106
118
  console.error('[REGISTER AUTO ERROR]', error.message);
@@ -114,13 +126,13 @@ router.post('/proxy/register', requireLocalOnly, async (req, res) => {
114
126
  * Enregistrement manuel via l'interface graphique (port obligatoire)
115
127
  */
116
128
  router.post('/proxy/register-manual', requireAuth, async (req, res) => {
117
- const { path, name, port, target, healthCheck } = req.body;
129
+ const { path, name, port, target, healthCheck, group } = req.body;
118
130
 
119
131
  if (!path) return res.status(400).json({ success: false, error: 'Path is required' });
120
132
  if (!port) return res.status(400).json({ success: false, error: 'Port is required for manual registration' });
121
133
 
122
134
  try {
123
- const result = await registerRouteInternal({ path, name, port, target, healthCheck, autoAssignPort: false });
135
+ const result = await registerRouteInternal({ path, name, port, target, healthCheck, group, autoAssignPort: false });
124
136
  res.json(result);
125
137
  } catch (error) {
126
138
  console.error('[REGISTER MANUAL ERROR]', error.message);
@@ -178,6 +190,7 @@ router.get('/proxy/routes', requireAuth, (req, res) => {
178
190
  target: value.target || 'http://localhost',
179
191
  name: value.name,
180
192
  healthCheck: value.healthCheck,
193
+ group: value.group || null,
181
194
  registeredAt: value.registeredAt,
182
195
  fullTarget: `${value.target || 'http://localhost'}:${value.port}`
183
196
  });
@@ -1,99 +1,99 @@
1
- const { createProxyMiddleware } = require('http-proxy-middleware');
2
- const { registry } = require('./registry');
3
- const { findRouteForPath } = require('./portManager');
4
- const { unregisterServer } = require('./healthCheck');
5
- const { requireAuth } = require('./auth');
6
-
7
- /**
8
- * Échappe les caractères HTML pour éviter les injections XSS
9
- */
10
- function escapeHtml(text) {
11
- const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
12
- return String(text).replace(/[&<>"']/g, char => map[char]);
13
- }
14
-
15
- /**
16
- * Middleware de proxy persistant — créé une seule fois pour éviter les fuites mémoire
17
- */
18
- const persistentProxyMiddleware = createProxyMiddleware({
19
- target: 'http://localhost',
20
- router: (req) => req.proxyTarget,
21
- changeOrigin: true,
22
- pathRewrite: (path, req) => {
23
- const routePath = req.proxyRoutePath;
24
- if (routePath && path.startsWith(routePath)) {
25
- return path.slice(routePath.length) || '/';
26
- }
27
- return path;
28
- },
29
- on: {
30
- proxyReq: (proxyReq, req) => {
31
- if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {
32
- const bodyData = JSON.stringify(req.body);
33
- proxyReq.setHeader('Content-Type', 'application/json');
34
- proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
35
- proxyReq.write(bodyData);
36
- }
37
- console.log(`[PROXY] ${req.method} ${req.path} -> ${req.proxyTarget}`);
38
- },
39
- error: (err, req, res) => {
40
- if (res.headersSent) return;
41
- console.error(`[PROXY ERROR] ${req.path}:`, err.message);
42
-
43
- if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
44
- const route = findRouteForPath(req.path);
45
- if (route) {
46
- const routePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
47
- if (routePath) {
48
- unregisterServer(routePath, `connection error (${err.code})`).catch(() => {});
49
- console.log(`[PROXY] Auto-unregistered ${routePath} due to connection error`);
50
- }
51
- }
52
- }
53
-
54
- res.status(502).json({ error: 'Proxy error' });
55
- }
56
- }
57
- });
58
-
59
- /**
60
- * Middleware principal de routage vers le proxy
61
- * Inclut le Smart Routing (Referer Fallback)
62
- */
63
- function routingMiddleware(req, res, next) {
64
- if (req.path.startsWith('/proxy/')) return next();
65
-
66
- requireAuth(req, res, () => {
67
- let route = findRouteForPath(req.path);
68
-
69
- if (!route && req.get('Referer')) {
70
- try {
71
- const refererUrl = new URL(req.get('Referer'));
72
- if (refererUrl.host === req.get('host')) {
73
- const refererPath = refererUrl.pathname;
74
- route = findRouteForPath(refererPath);
75
- if (route) {
76
- console.log(`[SMART ROUTE] Inferred ${route.name} from referer: ${refererPath}`);
77
- }
78
- }
79
- } catch {
80
- // URL de referer invalide, on ignore
81
- }
82
- }
83
-
84
- if (!route) {
85
- return res.status(404).json({
86
- error: 'No route registered for this path',
87
- path: req.path,
88
- hint: 'Register a route with POST /proxy/register or use the UI'
89
- });
90
- }
91
-
92
- const target = route.target || 'http://localhost';
93
- req.proxyTarget = `${target}:${route.port}`;
94
- req.proxyRoutePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
95
- persistentProxyMiddleware(req, res, next);
96
- });
97
- }
98
-
99
- module.exports = { persistentProxyMiddleware, routingMiddleware, escapeHtml };
1
+ const { createProxyMiddleware } = require('http-proxy-middleware');
2
+ const { registry } = require('./registry');
3
+ const { findRouteForPath } = require('./portManager');
4
+ const { unregisterServer } = require('./healthCheck');
5
+ const { requireAuth } = require('./auth');
6
+
7
+ /**
8
+ * Échappe les caractères HTML pour éviter les injections XSS
9
+ */
10
+ function escapeHtml(text) {
11
+ const map = { '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#039;' };
12
+ return String(text).replace(/[&<>"']/g, char => map[char]);
13
+ }
14
+
15
+ /**
16
+ * Middleware de proxy persistant — créé une seule fois pour éviter les fuites mémoire
17
+ */
18
+ const persistentProxyMiddleware = createProxyMiddleware({
19
+ target: 'http://localhost',
20
+ router: (req) => req.proxyTarget,
21
+ changeOrigin: true,
22
+ pathRewrite: (path, req) => {
23
+ const routePath = req.proxyRoutePath;
24
+ if (routePath && path.startsWith(routePath)) {
25
+ return path.slice(routePath.length) || '/';
26
+ }
27
+ return path;
28
+ },
29
+ on: {
30
+ proxyReq: (proxyReq, req) => {
31
+ if (req.body && typeof req.body === 'object' && Object.keys(req.body).length > 0) {
32
+ const bodyData = JSON.stringify(req.body);
33
+ proxyReq.setHeader('Content-Type', 'application/json');
34
+ proxyReq.setHeader('Content-Length', Buffer.byteLength(bodyData));
35
+ proxyReq.write(bodyData);
36
+ }
37
+ console.log(`[PROXY] ${req.method} ${req.path} -> ${req.proxyTarget}`);
38
+ },
39
+ error: (err, req, res) => {
40
+ if (res.headersSent) return;
41
+ console.error(`[PROXY ERROR] ${req.path}:`, err.message);
42
+
43
+ if (err.code === 'ECONNREFUSED' || err.code === 'ETIMEDOUT') {
44
+ const route = findRouteForPath(req.path);
45
+ if (route) {
46
+ const routePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
47
+ if (routePath) {
48
+ unregisterServer(routePath, `connection error (${err.code})`).catch(() => {});
49
+ console.log(`[PROXY] Auto-unregistered ${routePath} due to connection error`);
50
+ }
51
+ }
52
+ }
53
+
54
+ res.status(502).json({ error: 'Proxy error' });
55
+ }
56
+ }
57
+ });
58
+
59
+ /**
60
+ * Middleware principal de routage vers le proxy
61
+ * Inclut le Smart Routing (Referer Fallback)
62
+ */
63
+ function routingMiddleware(req, res, next) {
64
+ if (req.path.startsWith('/proxy/')) return next();
65
+
66
+ requireAuth(req, res, () => {
67
+ let route = findRouteForPath(req.path);
68
+
69
+ if (!route && req.get('Referer')) {
70
+ try {
71
+ const refererUrl = new URL(req.get('Referer'));
72
+ if (refererUrl.host === req.get('host')) {
73
+ const refererPath = refererUrl.pathname;
74
+ route = findRouteForPath(refererPath);
75
+ if (route) {
76
+ console.log(`[SMART ROUTE] Inferred ${route.name} from referer: ${refererPath}`);
77
+ }
78
+ }
79
+ } catch {
80
+ // URL de referer invalide, on ignore
81
+ }
82
+ }
83
+
84
+ if (!route) {
85
+ return res.status(404).json({
86
+ error: 'No route registered for this path',
87
+ path: req.path,
88
+ hint: 'Register a route with POST /proxy/register or use the UI'
89
+ });
90
+ }
91
+
92
+ const target = route.target || 'http://localhost';
93
+ req.proxyTarget = `${target}:${route.port}`;
94
+ req.proxyRoutePath = [...registry.routes.entries()].find(([, v]) => v === route)?.[0];
95
+ persistentProxyMiddleware(req, res, next);
96
+ });
97
+ }
98
+
99
+ module.exports = { persistentProxyMiddleware, routingMiddleware, escapeHtml };
@@ -31,7 +31,7 @@ class Mutex {
31
31
  }
32
32
 
33
33
  const registry = {
34
- routes: new Map(), // path -> { port, target, name, healthCheck, registeredAt }
34
+ routes: new Map(), // path -> { port, target, name, healthCheck, group, registeredAt }
35
35
  usedPorts: new Set(), // Ports déjà attribués
36
36
  nextPort: INTERNAL_PORT_START
37
37
  };
@@ -1,5 +1,65 @@
1
1
  const { escapeHtml } = require('../services/proxyRouter');
2
2
 
3
+ /** Libellé de la section pour les applications sans groupe (APP_GROUP absent). */
4
+ const UNGROUPED_LABEL = 'Non groupées';
5
+
6
+ /**
7
+ * Répartit les routes par groupe.
8
+ * @param {Array} routes
9
+ * @returns {Array<{ name: string, routes: Array }>} groupes ordonnés :
10
+ * groupes nommés triés alphabétiquement, section "non groupées" en dernier.
11
+ */
12
+ function groupRoutes(routes) {
13
+ const groups = new Map();
14
+ for (const route of routes) {
15
+ const key = (route.group && String(route.group).trim()) || UNGROUPED_LABEL;
16
+ if (!groups.has(key)) groups.set(key, []);
17
+ groups.get(key).push(route);
18
+ }
19
+
20
+ return [...groups.entries()]
21
+ .map(([name, groupRoutes]) => ({ name, routes: groupRoutes }))
22
+ .sort((a, b) => {
23
+ if (a.name === UNGROUPED_LABEL) return 1;
24
+ if (b.name === UNGROUPED_LABEL) return -1;
25
+ return a.name.localeCompare(b.name);
26
+ });
27
+ }
28
+
29
+ /**
30
+ * Enveloppe des cartes de service dans des sections repliables (`<details>`),
31
+ * repliées par défaut. S'il n'existe qu'une seule section "non groupées",
32
+ * les cartes sont rendues à plat (aucun groupe explicite à afficher).
33
+ * @param {Array} routes
34
+ * @param {(route: any) => string} renderCard
35
+ * @returns {string}
36
+ */
37
+ function renderGroupedServices(routes, renderCard) {
38
+ const groups = groupRoutes(routes);
39
+ const onlyUngrouped = groups.length === 1 && groups[0].name === UNGROUPED_LABEL;
40
+
41
+ if (onlyUngrouped) {
42
+ return `<div class="services-list">
43
+ ${groups[0].routes.map(renderCard).join('')}
44
+ </div>`;
45
+ }
46
+
47
+ return `<div class="service-groups">
48
+ ${groups.map(group => `
49
+ <details class="service-group"${group.name === UNGROUPED_LABEL ? ' open' : ''}>
50
+ <summary class="service-group-summary">
51
+ <span class="service-group-caret">&#9654;</span>
52
+ <span class="service-group-name">${escapeHtml(group.name)}</span>
53
+ <span class="service-group-count">${group.routes.length}</span>
54
+ </summary>
55
+ <div class="services-list">
56
+ ${group.routes.map(renderCard).join('')}
57
+ </div>
58
+ </details>
59
+ `).join('')}
60
+ </div>`;
61
+ }
62
+
3
63
  /**
4
64
  * Page dashboard principale
5
65
  * @param {Array} routes - liste des routes enregistrées
@@ -11,9 +71,7 @@ const { escapeHtml } = require('../services/proxyRouter');
11
71
  */
12
72
  function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpCommand, minimalDashboard) {
13
73
  if (minimalDashboard) {
14
- const servicesHtml = routes.length > 0
15
- ? `<div class="services-list">
16
- ${routes.map(route => `
74
+ const renderMinimalCard = (route) => `
17
75
  <div class="service-card">
18
76
  <div class="service-info">
19
77
  <span class="service-name">${escapeHtml(route.name)}</span>
@@ -22,8 +80,10 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
22
80
  <a href="${escapeHtml(route.path)}" class="action-btn primary" title="Acc&eacute;der" target="_blank" rel="noopener noreferrer">Ouvrir</a>
23
81
  </div>
24
82
  </div>
25
- `).join('')}
26
- </div>`
83
+ `;
84
+
85
+ const servicesHtml = routes.length > 0
86
+ ? renderGroupedServices(routes, renderMinimalCard)
27
87
  : `<div class="empty-state minimal-empty">
28
88
  <p>Aucun service</p>
29
89
  </div>`;
@@ -52,6 +112,38 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
52
112
  -webkit-text-fill-color: transparent;
53
113
  background-clip: text;
54
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; }
55
147
  .services-list { display: flex; flex-direction: column; gap: 0.5rem; }
56
148
  .service-card {
57
149
  background: rgba(255, 255, 255, 0.05);
@@ -118,9 +210,7 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
118
210
  </html>`;
119
211
  }
120
212
 
121
- const servicesHtml = routes.length > 0
122
- ? `<div class="services-list">
123
- ${routes.map(route => `
213
+ const renderServiceCard = (route) => `
124
214
  <div class="service-card" data-path="${escapeHtml(route.path)}">
125
215
  <div class="service-info">
126
216
  <span class="service-name">${escapeHtml(route.name)}</span>
@@ -135,8 +225,10 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
135
225
  <button onclick="killPort(${route.port}, this)" class="action-btn danger" title="Tuer le process">Tuer</button>
136
226
  </div>
137
227
  </div>
138
- `).join('')}
139
- </div>`
228
+ `;
229
+
230
+ const servicesHtml = routes.length > 0
231
+ ? renderGroupedServices(routes, renderServiceCard)
140
232
  : `<div class="empty-state">
141
233
  <h2>Aucun service enregistr&eacute;</h2>
142
234
  <p>Cliquez sur "+ Ajouter" ou utilisez l'API:</p>
@@ -371,6 +463,51 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
371
463
  .modal .submit-btn:hover {
372
464
  opacity: 0.9;
373
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; }
374
511
  .services-list {
375
512
  display: flex;
376
513
  flex-direction: column;
@@ -603,6 +740,10 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
603
740
  <label for="port">Port</label>
604
741
  <input type="number" id="port" name="port" placeholder="4000" required>
605
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 &laquo; Non group&eacute;es &raquo;">
746
+ </div>
606
747
  <div class="checkbox-group">
607
748
  <input type="checkbox" id="healthCheck" name="healthCheck">
608
749
  <label for="healthCheck">Health check actif</label>
@@ -669,6 +810,7 @@ function renderDashboard(routes, availablePorts, proxyName, hasAuth, cleanUpComm
669
810
  name: document.getElementById('name').value,
670
811
  target: document.getElementById('target').value,
671
812
  port: parseInt(document.getElementById('port').value),
813
+ group: document.getElementById('group').value,
672
814
  healthCheck: document.getElementById('healthCheck').checked
673
815
  };
674
816