@imenam/self-register-proxy 1.0.42 → 1.0.45

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.
@@ -3,386 +3,137 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>{{PROXY_NAME}} - Agent</title>
6
+ <title>{{PROXY_NAME}} — Agent</title>
7
7
  <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/xterm/css/xterm.css">
8
8
  <style>
9
- * { margin: 0; padding: 0; box-sizing: border-box; }
10
- html, body { height: 100%; overflow: hidden; background: #1e1e1e; }
11
- .terminal-header {
12
- display: flex;
13
- align-items: center;
14
- gap: 1rem;
15
- padding: 0.5rem 1rem;
16
- background: #1a1a2e;
17
- border-bottom: 1px solid rgba(255,255,255,0.1);
18
- color: #e4e4e7;
19
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
20
- font-size: 0.85rem;
21
- }
22
- .back-link {
23
- color: #8b5cf6;
24
- text-decoration: none;
25
- font-weight: 500;
26
- }
27
- .back-link:hover {
28
- text-decoration: underline;
29
- }
30
- .terminal-title {
31
- font-weight: 600;
32
- color: #f4f4f5;
9
+ {{BASE_CSS}}
10
+ body { height: 100vh; display: flex; flex-direction: column; overflow: hidden; background: #0B0B0F; }
11
+
12
+ /* Barre supérieure */
13
+ .term-bar {
14
+ display: flex; align-items: center; gap: 16px; height: 48px; padding: 0 16px;
15
+ background: var(--bg); border-bottom: 1px solid var(--border); flex-shrink: 0;
33
16
  }
17
+ .term-back { display: grid; place-items: center; width: 28px; height: 28px; border-radius: 6px; color: var(--text-2); }
18
+ .term-back:hover { background: var(--surface); color: var(--text); }
19
+ .term-title { font-size: 15px; font-weight: 600; }
34
20
  .terminal-command {
35
- color: #10b981;
36
- font-family: 'SF Mono', 'Fira Code', monospace;
37
- font-size: 0.75rem;
38
- margin-left: auto;
39
- overflow: hidden;
40
- text-overflow: ellipsis;
41
- white-space: nowrap;
42
- }
43
- .layout-body {
44
- display: flex;
45
- height: calc(100vh - 41px);
46
- overflow: hidden;
47
- position: relative;
48
- }
49
- #terminal-container {
50
- flex: 1;
51
- min-width: 0;
52
- }
53
- /* --- Sidebar méta-config --- */
54
- .meta-sidebar {
55
- width: 240px;
56
- flex-shrink: 0;
57
- background: #12121f;
58
- border-right: 1px solid rgba(255,255,255,0.08);
59
- display: flex;
60
- flex-direction: column;
61
- overflow: hidden;
62
- transition: width 0.2s ease;
63
- }
64
- .meta-sidebar.collapsed { width: 0; }
65
- .sidebar-header {
66
- display: flex;
67
- align-items: center;
68
- justify-content: space-between;
69
- padding: 0.5rem 0.75rem;
70
- border-bottom: 1px solid rgba(255,255,255,0.08);
71
- flex-shrink: 0;
72
- }
73
- .sidebar-title { font-size: 0.8rem; font-weight: 600; color: #a1a1aa; white-space: nowrap; }
74
- .sidebar-collapse-btn {
75
- background: transparent; border: none; color: #71717a;
76
- cursor: pointer; font-size: 1.1rem; line-height: 1; padding: 0.1rem 0.3rem;
77
- }
78
- .sidebar-collapse-btn:hover { color: #e4e4e7; }
79
- .sidebar-expand-btn {
80
- position: absolute; left: 0; top: 50%;
81
- transform: translateY(-50%);
82
- background: #12121f; border: 1px solid rgba(255,255,255,0.1);
83
- border-left: none; border-radius: 0 6px 6px 0;
84
- color: #71717a; cursor: pointer;
85
- padding: 0.5rem 0.3rem; z-index: 10;
86
- display: none; font-size: 1rem; line-height: 1;
87
- }
88
- .sidebar-expand-btn:hover { color: #e4e4e7; }
89
- .sidebar-sync-warning {
90
- font-size: 0.7rem; color: #fbbf24;
91
- background: rgba(245,158,11,0.12);
92
- padding: 0.3rem 0.75rem;
93
- border-bottom: 1px solid rgba(245,158,11,0.3);
94
- white-space: nowrap; overflow: hidden; flex-shrink: 0;
95
- }
96
- .sidebar-toolbar {
97
- display: flex; gap: 0.4rem; padding: 0.5rem 0.6rem;
98
- border-bottom: 1px solid rgba(255,255,255,0.06);
99
- flex-shrink: 0;
100
- }
101
- .sb-btn {
102
- border: none; border-radius: 5px; padding: 0.3rem 0.6rem;
103
- font-size: 0.75rem; font-weight: 600; cursor: pointer; color: white;
104
- transition: opacity 0.2s; white-space: nowrap;
105
- }
106
- .sb-btn:hover { opacity: 0.85; }
107
- .sb-btn:disabled { opacity: 0.35; cursor: not-allowed; }
108
- .sb-btn-primary { background: linear-gradient(90deg, #6366f1, #8b5cf6); }
109
- .sb-btn-amber { background: linear-gradient(90deg, #f59e0b, #d97706); }
110
- .sb-btn-green { background: linear-gradient(90deg, #10b981, #059669); }
111
- .sb-btn-red { background: rgba(239,68,68,0.8); }
112
- .sb-btn-ghost { background: rgba(255,255,255,0.1); border: 1px solid rgba(255,255,255,0.15); }
113
- .sidebar-configs {
114
- flex: 1; overflow-y: auto; padding: 0.5rem;
115
- display: flex; flex-direction: column; gap: 0.4rem;
116
- }
117
- .sidebar-empty { font-size: 0.75rem; color: #52525b; text-align: center; padding: 1rem 0; }
118
- .sb-config-card {
119
- background: rgba(255,255,255,0.05);
120
- border: 1px solid rgba(255,255,255,0.08);
121
- border-left: 3px solid transparent;
122
- border-radius: 6px; padding: 0.45rem 0.6rem;
123
- }
124
- .sb-config-card.active {
125
- border-color: rgba(16,185,129,0.4);
126
- border-left-color: #10b981;
127
- background: rgba(16,185,129,0.08);
128
- }
129
- .sb-config-row {
130
- display: flex; align-items: center; gap: 0.4rem;
131
- }
132
- .sb-config-name {
133
- flex: 1; font-size: 0.8rem; font-weight: 600; color: #f4f4f5;
134
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
135
- }
136
- .sb-config-actions { display: flex; gap: 0.25rem; flex-shrink: 0; }
137
- .sb-icon-btn {
138
- border: none; border-radius: 4px; width: 24px; height: 24px;
139
- display: inline-flex; align-items: center; justify-content: center;
140
- font-size: 0.75rem; cursor: pointer; color: white; transition: opacity 0.2s;
141
- flex-shrink: 0; padding: 0;
142
- }
143
- .sb-icon-btn:hover { opacity: 0.8; }
144
- .sb-icon-btn:disabled { opacity: 0.3; cursor: not-allowed; }
145
- .sb-icon-btn-green { background: linear-gradient(135deg, #10b981, #059669); }
146
- .sb-icon-btn-ghost { background: rgba(255,255,255,0.12); border: 1px solid rgba(255,255,255,0.15); }
147
- .sb-icon-btn-red { background: rgba(239,68,68,0.75); }
148
- .sb-icon-btn-blue { background: rgba(99,102,241,0.75); }
149
- .sb-icon-btn-amber { background: rgba(245,158,11,0.75); }
150
- .sb-icon-btn-purple { background: linear-gradient(135deg, #6366f1, #8b5cf6); }
151
- .sb-section-label {
152
- font-size: 0.65rem; font-weight: 700; letter-spacing: 0.08em;
153
- color: #52525b; text-transform: uppercase;
154
- padding: 0.6rem 0.2rem 0.3rem; margin-top: 0.2rem;
155
- border-top: 1px solid rgba(255,255,255,0.06);
156
- display: flex; align-items: center; justify-content: space-between;
157
- }
158
- .sb-refresh-btn {
159
- background: transparent; border: none; color: #52525b;
160
- cursor: pointer; font-size: 0.85rem; line-height: 1; padding: 0.1rem;
161
- transition: color 0.2s, transform 0.3s;
162
- }
163
- .sb-refresh-btn:hover { color: #a1a1aa; }
164
- .sb-refresh-btn.spinning { animation: spin 0.6s linear infinite; }
165
- .sb-service-card {
166
- display: flex; align-items: center; gap: 0.4rem;
167
- padding: 0.35rem 0.5rem;
168
- background: rgba(255,255,255,0.03);
169
- border: 1px solid rgba(255,255,255,0.06);
170
- border-radius: 5px;
171
- cursor: pointer;
172
- transition: background 0.15s, border-color 0.15s;
173
- }
174
- .sb-service-card:hover {
175
- background: rgba(255,255,255,0.06);
176
- border-color: rgba(139,92,246,0.3);
177
- }
178
- .sb-service-name {
179
- flex: 1; font-size: 0.78rem; font-weight: 500; color: #d4d4d8;
180
- overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
181
- }
182
- .sb-service-actions { display: flex; gap: 0.25rem; flex-shrink: 0; }
183
- /* Modal méta-config (partagée) */
184
- .sb-modal-overlay {
185
- display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
186
- background: rgba(0,0,0,0.6);
187
- z-index: 1000; align-items: center; justify-content: center;
188
- }
189
- .sb-modal-overlay.active { display: flex; }
190
- .sb-modal {
191
- background: linear-gradient(135deg, #1e1e3f 0%, #1a1a2e 100%);
192
- border: 1px solid rgba(255,255,255,0.15); border-radius: 12px;
193
- padding: 1.5rem; width: 90%; max-width: 480px;
194
- box-shadow: 0 20px 40px rgba(0,0,0,0.4); max-height: 90vh; overflow-y: auto;
195
- }
196
- .sb-modal-header {
197
- display: flex; justify-content: space-between; align-items: center;
198
- margin-bottom: 1rem;
199
- }
200
- .sb-modal-header h2 { font-size: 1.1rem; color: #f4f4f5; }
201
- .sb-modal-close {
202
- background: transparent; border: none; color: #71717a;
203
- font-size: 1.25rem; cursor: pointer; padding: 0.25rem; line-height: 1;
204
- }
205
- .sb-modal-close:hover { color: #e4e4e7; }
206
- .sb-form-group { margin-bottom: 0.75rem; }
207
- .sb-form-group label { font-size: 0.75rem; color: #a1a1aa; margin-bottom: 0.25rem; display: block; }
208
- .sb-form-group input, .sb-form-group textarea {
209
- background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1);
210
- padding: 0.5rem 0.75rem; border-radius: 6px; color: white;
211
- font-family: inherit; font-size: 0.9rem; width: 100%;
212
- }
213
- .sb-form-group textarea { resize: vertical; min-height: 60px; }
214
- .sb-form-group input:focus, .sb-form-group textarea:focus { outline: none; border-color: #8b5cf6; }
215
- .sb-modal-mappings { margin-top: 0.75rem; }
216
- .sb-modal-mappings-title { font-size: 0.8rem; font-weight: 600; color: #a1a1aa; margin-bottom: 0.5rem; }
217
- .sb-modal-mappings-list { display: flex; flex-direction: column; gap: 0.4rem; }
218
- .sb-modal-mapping-item {
219
- display: flex; align-items: center; gap: 0.5rem;
220
- background: rgba(0,0,0,0.2); padding: 0.4rem 0.6rem; border-radius: 6px;
221
- }
222
- .sb-modal-mapping-item .mcp-label { flex: 1; font-size: 0.8rem; color: #f4f4f5; font-weight: 500; }
223
- .sb-modal-mapping-item .mcp-path-label { font-size: 0.7rem; color: #8b5cf6; font-family: 'SF Mono', 'Fira Code', monospace; }
224
- .sb-modal-mapping-item select {
225
- background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.15);
226
- color: #e4e4e7; padding: 0.3rem 0.5rem; border-radius: 4px; font-size: 0.8rem; min-width: 130px;
227
- }
228
- .sb-modal-mapping-item select:focus { outline: none; border-color: #8b5cf6; }
229
- .sb-spinner {
230
- display: inline-block; width: 12px; height: 12px;
231
- border: 2px solid rgba(255,255,255,0.2); border-top-color: #8b5cf6;
232
- border-radius: 50%; animation: spin 0.6s linear infinite;
233
- }
234
- .prompt-btn {
235
- background: linear-gradient(90deg, #6366f1, #8b5cf6);
236
- color: white;
237
- border: none;
238
- padding: 0.4rem 0.8rem;
239
- border-radius: 6px;
240
- font-weight: 600;
241
- font-size: 0.8rem;
242
- cursor: pointer;
243
- transition: opacity 0.2s;
244
- }
245
- .prompt-btn:hover { opacity: 0.9; }
246
- .modal-overlay {
247
- display: none; position: fixed; top: 0; left: 0; right: 0; bottom: 0;
248
- background: rgba(0,0,0,0.6);
249
- z-index: 1000; align-items: center; justify-content: center;
250
- }
251
- .modal-overlay.active { display: flex; }
252
- .modal {
253
- background: linear-gradient(135deg, #1e1e3f 0%, #1a1a2e 100%);
254
- border: 1px solid rgba(255,255,255,0.15); border-radius: 12px;
255
- padding: 1.5rem; width: 90%; max-width: 640px;
256
- box-shadow: 0 20px 40px rgba(0,0,0,0.4);
257
- display: flex; flex-direction: column; gap: 1rem;
258
- }
259
- .modal-header {
260
- display: flex; justify-content: space-between; align-items: center;
261
- }
262
- .modal-header h2 { font-size: 1.1rem; color: #f4f4f5; }
263
- .modal-close {
264
- background: transparent; border: none; color: #71717a;
265
- font-size: 1.25rem; cursor: pointer; padding: 0.25rem; line-height: 1;
266
- }
267
- .modal-close:hover { color: #e4e4e7; }
268
- #promptTextarea {
269
- background: rgba(0,0,0,0.3); border: 1px solid rgba(255,255,255,0.1);
270
- border-radius: 6px; color: white; font-family: inherit; font-size: 0.9rem;
271
- padding: 0.75rem; width: 100%; min-height: 200px; resize: vertical; line-height: 1.5;
272
- }
273
- #promptTextarea:focus { outline: none; border-color: #8b5cf6; }
274
- .modal-actions { display: flex; justify-content: flex-end; gap: 0.5rem; }
275
- .btn-cancel {
276
- background: rgba(255,255,255,0.08); color: #e4e4e7; border: none;
277
- padding: 0.5rem 1rem; border-radius: 6px; font-size: 0.85rem; cursor: pointer;
278
- }
279
- .btn-send {
280
- background: linear-gradient(90deg, #6366f1, #8b5cf6); color: white; border: none;
281
- padding: 0.5rem 1rem; border-radius: 6px; font-weight: 600; font-size: 0.85rem; cursor: pointer;
282
- }
21
+ font-family: var(--mono); font-size: 12px; color: var(--text-3);
22
+ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; max-width: 320px;
23
+ }
24
+ .term-spacer { flex: 1 1 auto; }
25
+ .term-select {
26
+ display: flex; align-items: center; gap: 10px; height: 30px; padding: 0 10px 0 12px;
27
+ background: var(--surface); border: 1px solid var(--border); border-radius: 8px;
28
+ color: var(--text); font-family: inherit; font-size: 13px; cursor: pointer;
29
+ }
30
+ .term-select:hover { border-color: #3A3A45; }
31
+ .term-select svg { color: var(--text-3); }
32
+ .term-select-name { max-width: 160px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
33
+ .term-actions { display: flex; align-items: center; gap: 12px; }
34
+ .term-bar .btn { height: 30px; }
35
+ .menu-scroll { max-height: 320px; overflow-y: auto; }
36
+ .menu-empty { font-size: 12px; color: var(--text-3); padding: 8px; }
37
+
38
+ /* Terminal */
39
+ .term-body { flex: 1 1 auto; min-height: 0; padding: 12px 16px 0; }
40
+ #terminal-container { width: 100%; height: 100%; }
41
+ .xterm-viewport::-webkit-scrollbar { width: 10px; }
42
+ .xterm-viewport::-webkit-scrollbar-thumb { background: #2A2A33; border-radius: 5px; }
43
+
44
+ /* Barre de saisie */
45
+ .term-input {
46
+ flex-shrink: 0; margin: 12px 16px 16px; padding: 8px 8px 8px 16px;
47
+ display: flex; align-items: flex-end; gap: 12px;
48
+ background: var(--surface); border: 1px solid var(--border); border-radius: 12px;
49
+ }
50
+ .term-input:focus-within { border-color: #3A3A45; }
51
+ .term-input textarea {
52
+ flex: 1 1 auto; background: none; border: 0; padding: 8px 0; resize: none;
53
+ color: var(--text); font-family: inherit; font-size: 14px; line-height: 1.5;
54
+ max-height: 160px; overflow-y: auto;
55
+ }
56
+ .term-input textarea:focus { outline: none; border: 0; }
57
+ .term-hint { font-size: 12px; color: var(--text-4); padding-bottom: 12px; white-space: nowrap; }
58
+ .term-send {
59
+ flex-shrink: 0; width: 36px; height: 36px; border-radius: 10px; border: 0;
60
+ background: var(--accent); color: #fff; display: grid; place-items: center; cursor: pointer;
61
+ }
62
+ .term-send:hover { background: #5457E5; }
63
+
64
+ /* Autocomplete @ */
283
65
  .file-dropdown {
284
- display: none; position: fixed; z-index: 1100;
285
- background: #1a1a2e; border: 1px solid rgba(255,255,255,0.15);
286
- border-radius: 6px; overflow-y: auto; max-height: 180px;
287
- box-shadow: 0 8px 24px rgba(0,0,0,0.4);
66
+ position: fixed; z-index: 200; display: none; max-height: 260px; overflow-y: auto;
67
+ background: var(--surface-2); border: 1px solid #2E2E38; border-radius: 10px; padding: 6px;
68
+ box-shadow: 0 16px 40px rgba(0,0,0,.55);
288
69
  }
289
70
  .file-dropdown-item {
290
- padding: 0.4rem 0.75rem; cursor: pointer;
291
- font-size: 0.8rem; color: #e4e4e7;
292
- font-family: 'SF Mono', 'Fira Code', monospace;
293
- white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
71
+ padding: 6px 10px; border-radius: 6px; font-size: 13px; color: var(--text-2);
72
+ cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
294
73
  }
295
- .file-dropdown-item.active { background: rgba(139,92,246,0.35); color: white; }
296
- .file-dropdown-item:hover { background: rgba(255,255,255,0.08); }
74
+ .file-dropdown-item:hover, .file-dropdown-item.active { background: #26262E; color: var(--text); }
297
75
  .file-path-hint {
298
- display: none; position: fixed; z-index: 1101;
299
- background: #0f0f1a; border: 1px solid rgba(139,92,246,0.4);
300
- border-radius: 4px; padding: 0.3rem 0.6rem;
301
- font-size: 0.75rem; font-family: 'SF Mono', 'Fira Code', monospace;
302
- color: #8b5cf6; white-space: nowrap; pointer-events: none;
303
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
76
+ position: fixed; z-index: 201; display: none; padding: 5px 10px;
77
+ background: var(--surface-2); border: 1px solid #2E2E38; border-radius: 6px;
78
+ font-family: var(--mono); font-size: 11px; color: var(--text-3); pointer-events: none;
79
+ }
80
+
81
+ @media (max-width: 760px) {
82
+ .terminal-command, .term-hint { display: none; }
83
+ .term-bar { gap: 10px; padding: 0 10px; }
84
+ .term-select-name { max-width: 90px; }
304
85
  }
305
86
  </style>
306
87
  </head>
307
88
  <body>
308
- <div class="terminal-header">
309
- <a href="/" class="back-link">&larr; Dashboard</a>
310
- <span class="terminal-title">Agent</span>
311
- <button class="prompt-btn" onclick="openPromptModal()">&#9998; Prompt</button>
89
+ <header class="term-bar">
90
+ <a class="term-back" href="/" title="Retour au dashboard" aria-label="Retour au dashboard">
91
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m12 19-7-7 7-7"/><path d="M19 12H5"/></svg>
92
+ </a>
93
+ <span class="term-title">Agent</span>
312
94
  {{TERMINAL_COMMAND}}
313
- </div>
314
- <div class="modal-overlay" id="promptModal" onclick="if(event.target===this)closePromptModal()">
315
- <div class="modal">
316
- <div class="modal-header">
317
- <h2>Saisir un prompt</h2>
318
- <button class="modal-close" onclick="closePromptModal()">&times;</button>
319
- </div>
320
- <textarea id="promptTextarea" placeholder="Saisissez votre prompt&#8230;"></textarea>
321
- <div class="modal-actions">
322
- <button class="btn-cancel" onclick="closePromptModal()">Annuler</button>
323
- <button class="btn-send" onclick="sendPrompt()">Envoyer</button>
324
- </div>
325
- </div>
326
- </div>
327
- <div id="fileDropdown" class="file-dropdown"></div>
328
- <div id="filePathHint" class="file-path-hint"></div>
329
95
 
330
- <div class="layout-body">
331
- <aside class="meta-sidebar" id="metaSidebar">
332
- <div class="sidebar-header">
333
- <span class="sidebar-title">M&eacute;ta-configs</span>
334
- <button class="sidebar-collapse-btn" onclick="toggleSidebar()" title="R&eacute;duire">&#8249;</button>
335
- </div>
336
- <div class="sidebar-sync-warning" id="sidebarSyncWarning" style="display:none;">&#9888; D&eacute;synchronis&eacute;</div>
337
- <div class="sidebar-toolbar">
338
- <button class="sb-btn sb-btn-amber" id="sbDiscoverBtn" onclick="sidebarDiscover()">&#128269;</button>
339
- <button class="sb-btn sb-btn-primary" id="sbCreateBtn" onclick="openSbModal()" disabled>+</button>
340
- </div>
341
- <div class="sidebar-configs">
342
- <div id="sidebarConfigs"><p class="sidebar-empty">Chargement&hellip;</p></div>
343
- <div class="sb-section-label" id="sbServicesLabel">
344
- Services
345
- <button class="sb-refresh-btn" id="sbRefreshRoutesBtn" onclick="refreshSidebarRoutes()" title="Rafraîchir">&#8635;</button>
346
- </div>
347
- <div id="sidebarServices" style="display:flex;flex-direction:column;gap:0.3rem;"></div>
348
- </div>
349
- </aside>
96
+ <span class="term-spacer"></span>
350
97
 
351
- <button class="sidebar-expand-btn" id="sidebarExpandBtn" onclick="toggleSidebar()" title="M&eacute;ta-configs">&#9881;</button>
98
+ <div class="menu-wrap">
99
+ <button class="term-select" onclick="toggleMenu(this)" id="profileBtn">
100
+ <span class="dot" id="profileDot"></span>
101
+ <span class="term-select-name" id="profileName">Aucun profil</span>
102
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
103
+ </button>
104
+ <div class="menu" id="profileMenu"><div class="menu-empty">Chargement…</div></div>
105
+ </div>
352
106
 
353
- <div id="terminal-container"></div>
354
- </div>
107
+ <span class="term-spacer"></span>
355
108
 
356
- <div class="sb-modal-overlay" id="sbModalOverlay" onclick="if(event.target===this)closeSbModal()">
357
- <div class="sb-modal">
358
- <div class="sb-modal-header">
359
- <h2 id="sbModalTitle">Nouveau profil</h2>
360
- <button class="sb-modal-close" onclick="closeSbModal()">&times;</button>
109
+ <div class="term-actions">
110
+ <div class="menu-wrap">
111
+ <button class="term-select" onclick="toggleServicesMenu(this)">
112
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12.83 2.18a2 2 0 0 0-1.66 0L2.6 6.08a1 1 0 0 0 0 1.83l8.58 3.91a2 2 0 0 0 1.66 0l8.58-3.9a1 1 0 0 0 0-1.83z"/><path d="m22 17.65-9.17 4.16a2 2 0 0 1-1.66 0L2 17.65"/><path d="m22 12.65-9.17 4.16a2 2 0 0 1-1.66 0L2 12.65"/></svg>
113
+ <span>Services</span>
114
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>
115
+ </button>
116
+ <div class="menu menu-scroll" id="servicesMenu"><div class="menu-empty">Chargement…</div></div>
361
117
  </div>
362
- <form id="sbMetaForm" onsubmit="return sbHandleFormSubmit(event)">
363
- <input type="hidden" id="sbEditId" value="">
364
- <div class="sb-form-group">
365
- <label for="sbCfgId">Identifiant (slug)</label>
366
- <input type="text" id="sbCfgId" placeholder="mon-profil" required pattern="[a-z0-9\-]+" title="Lettres minuscules, chiffres et tirets uniquement">
367
- </div>
368
- <div class="sb-form-group">
369
- <label for="sbCfgName">Nom</label>
370
- <input type="text" id="sbCfgName" placeholder="Mon Profil" required>
371
- </div>
372
- <div class="sb-form-group">
373
- <label for="sbCfgDesc">Description</label>
374
- <textarea id="sbCfgDesc" placeholder="Description optionnelle..."></textarea>
375
- </div>
376
- <div class="sb-modal-mappings">
377
- <div class="sb-modal-mappings-title">Mappings MCP &rarr; Configuration</div>
378
- <div class="sb-modal-mappings-list" id="sbModalMappings">
379
- <p style="font-size:0.75rem;color:#71717a;">Lancez d&apos;abord la d&eacute;couverte des MCPs.</p>
380
- </div>
381
- </div>
382
- <button type="submit" class="sb-btn sb-btn-primary" style="width:100%;margin-top:1rem;" id="sbModalSubmitBtn">Cr&eacute;er</button>
383
- </form>
118
+ <button class="btn btn-primary" onclick="focusPrompt()">
119
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21.174 6.812a1 1 0 0 0-3.986-3.987L3.842 16.174a2 2 0 0 0-.5.83l-1.321 4.352a.5.5 0 0 0 .623.622l4.353-1.32a2 2 0 0 0 .83-.497z"/><path d="m15 5 4 4"/></svg>
120
+ <span>Prompt</span>
121
+ </button>
384
122
  </div>
385
- </div>
123
+ </header>
124
+
125
+ <main class="term-body"><div id="terminal-container"></div></main>
126
+
127
+ <footer class="term-input">
128
+ <textarea id="promptTextarea" rows="1" placeholder="Écrire un prompt…"></textarea>
129
+ <span class="term-hint">Ctrl+⏎ pour envoyer</span>
130
+ <button class="term-send" onclick="sendPrompt()" title="Envoyer" aria-label="Envoyer">
131
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m5 12 7-7 7 7"/><path d="M12 19V5"/></svg>
132
+ </button>
133
+ </footer>
134
+
135
+ <div id="fileDropdown" class="file-dropdown"></div>
136
+ <div id="filePathHint" class="file-path-hint"></div>
386
137
 
387
138
  <script src="https://cdn.jsdelivr.net/npm/xterm/lib/xterm.min.js"></script>
388
139
  <script src="https://cdn.jsdelivr.net/npm/xterm-addon-fit/lib/xterm-addon-fit.min.js"></script>
@@ -396,9 +147,13 @@
396
147
  ...options,
397
148
  terminalOptions: {
398
149
  cursorBlink: true,
150
+ fontSize: 13,
151
+ fontFamily: 'ui-monospace, SFMono-Regular, Consolas, Menlo, monospace',
399
152
  theme: {
400
- background: '#1e1e1e',
401
- foreground: '#d4d4d4',
153
+ background: '#0B0B0F',
154
+ foreground: '#D4D4D8',
155
+ cursor: '#818CF8',
156
+ selectionBackground: 'rgba(99,102,241,.35)',
402
157
  },
403
158
  ...options.terminalOptions,
404
159
  },
@@ -498,7 +253,54 @@
498
253
  wsPath: '/terminal-ws',
499
254
  });
500
255
 
501
- // --- Autocomplete @ (buffer-based) ---
256
+ function esc(str) {
257
+ const d = document.createElement('div');
258
+ d.textContent = String(str == null ? '' : str);
259
+ return d.innerHTML;
260
+ }
261
+
262
+ /* --- Menus : un seul ouvert à la fois --- */
263
+ function toggleMenu(btn) {
264
+ const menu = btn.nextElementSibling;
265
+ const wasOpen = menu.classList.contains('open');
266
+ closeAllMenus();
267
+ if (!wasOpen) menu.classList.add('open');
268
+ }
269
+
270
+ function closeAllMenus() {
271
+ document.querySelectorAll('.menu.open').forEach(m => m.classList.remove('open'));
272
+ }
273
+
274
+ document.addEventListener('click', (e) => {
275
+ if (!e.target.closest('.menu-wrap')) closeAllMenus();
276
+ });
277
+
278
+ /* --- Barre de saisie du prompt --- */
279
+ const _ta = document.getElementById('promptTextarea');
280
+
281
+ function focusPrompt() {
282
+ _ta.focus();
283
+ }
284
+
285
+ function autoGrow() {
286
+ _ta.style.height = 'auto';
287
+ _ta.style.height = Math.min(_ta.scrollHeight, 160) + 'px';
288
+ term.fit();
289
+ }
290
+
291
+ _ta.addEventListener('input', autoGrow);
292
+
293
+ function sendPrompt() {
294
+ const text = _ta.value;
295
+ if (!text.trim()) return;
296
+ term.ws.send(text);
297
+ _ta.value = '';
298
+ autoGrow();
299
+ _acHide();
300
+ term.terminal.focus();
301
+ }
302
+
303
+ /* --- Autocomplete @ (buffer-based) --- */
502
304
  let _acBuffer = null;
503
305
  let _acFiltered = [];
504
306
  let _acAtPos = -1;
@@ -523,10 +325,9 @@
523
325
  _acFiltered = files;
524
326
  _acIdx = -1;
525
327
  const el = document.getElementById('fileDropdown');
526
- const ta = document.getElementById('promptTextarea');
527
328
  el.innerHTML = '';
528
329
  if (!files.length) { el.style.display = 'none'; _acHidePathHint(); return; }
529
- files.forEach((file, i) => {
330
+ files.forEach((file) => {
530
331
  const d = document.createElement('div');
531
332
  d.className = 'file-dropdown-item';
532
333
  d.textContent = file.split('/').pop();
@@ -535,11 +336,12 @@
535
336
  d.addEventListener('mousedown', ev => { ev.preventDefault(); _acInsert(file); });
536
337
  el.appendChild(d);
537
338
  });
538
- const rect = ta.getBoundingClientRect();
539
- el.style.left = rect.left + 'px';
540
- el.style.top = (rect.bottom + 4) + 'px';
541
- el.style.width = Math.min(rect.width, 300) + 'px';
339
+ // Le dropdown s'ouvre vers le haut : la saisie est en bas de l'écran.
340
+ const rect = _ta.getBoundingClientRect();
542
341
  el.style.display = 'block';
342
+ el.style.left = rect.left + 'px';
343
+ el.style.width = Math.min(rect.width, 320) + 'px';
344
+ el.style.top = Math.max(8, rect.top - el.offsetHeight - 6) + 'px';
543
345
  }
544
346
 
545
347
  function _acHide() {
@@ -571,44 +373,18 @@
571
373
  }
572
374
 
573
375
  function _acInsert(file) {
574
- const ta = document.getElementById('promptTextarea');
575
- const end = ta.selectionStart;
576
- ta.value = ta.value.slice(0, _acAtPos) + '@' + file + ta.value.slice(end);
577
- ta.setSelectionRange(_acAtPos + 1 + file.length, _acAtPos + 1 + file.length);
578
- _acHide();
579
- ta.focus();
580
- }
581
-
582
- function openPromptModal() {
583
- document.getElementById('promptModal').classList.add('active');
584
- document.getElementById('promptTextarea').focus();
585
- }
586
-
587
- function closePromptModal() {
376
+ const end = _ta.selectionStart;
377
+ _ta.value = _ta.value.slice(0, _acAtPos) + '@' + file + _ta.value.slice(end);
378
+ _ta.setSelectionRange(_acAtPos + 1 + file.length, _acAtPos + 1 + file.length);
588
379
  _acHide();
589
- document.getElementById('promptModal').classList.remove('active');
590
- document.getElementById('promptTextarea').value = '';
380
+ _ta.focus();
591
381
  }
592
382
 
593
- function sendPrompt() {
594
- const text = document.getElementById('promptTextarea').value;
595
- if (!text) return;
596
- term.ws.send(text);
597
- closePromptModal();
598
- term.terminal.focus();
599
- }
600
-
601
- document.addEventListener('keydown', (e) => {
602
- if (e.key === 'Escape') { closePromptModal(); closeSbModal(); }
603
- if (e.ctrlKey && e.shiftKey && e.key === 'P') { e.preventDefault(); openPromptModal(); }
604
- });
605
-
606
- const _ta = document.getElementById('promptTextarea');
607
-
608
383
  _ta.addEventListener('keydown', e => {
609
- if (e.ctrlKey && e.key === 'Enter') { sendPrompt(); return; }
384
+ // Ctrl+Entrée envoie ; Entrée seule passe à la ligne.
385
+ if (e.ctrlKey && e.key === 'Enter') { e.preventDefault(); sendPrompt(); return; }
610
386
 
611
- const dropdownVisible = document.getElementById('fileDropdown').style.display !== 'none';
387
+ const dropdownVisible = document.getElementById('fileDropdown').style.display === 'block';
612
388
 
613
389
  if (dropdownVisible) {
614
390
  const items = [...document.getElementById('fileDropdown').querySelectorAll('.file-dropdown-item')];
@@ -630,6 +406,7 @@
630
406
  return;
631
407
  }
632
408
  if (e.key === 'Escape') {
409
+ e.preventDefault();
633
410
  _acHide();
634
411
  return;
635
412
  }
@@ -659,333 +436,137 @@
659
436
  }
660
437
  });
661
438
 
662
- _ta.addEventListener('blur', () => {
663
- setTimeout(() => _acHide(), 150);
664
- });
439
+ _ta.addEventListener('blur', () => { setTimeout(() => _acHide(), 150); });
665
440
 
666
- // =============================================
667
- // Sidebar méta-configurations
668
- // =============================================
669
- let sbDiscoveredMcps = [];
670
- let sbOfflineMappings = {};
671
- let sbActiveId = null;
672
- let sbSyncInterval = null;
673
- let sbSynced = true;
441
+ document.addEventListener('keydown', (e) => {
442
+ if (e.key === 'Escape') { closeAllMenus(); }
443
+ if (e.ctrlKey && e.shiftKey && e.key === 'P') { e.preventDefault(); focusPrompt(); }
444
+ });
674
445
 
675
- function sbEsc(str) {
676
- const d = document.createElement('div');
677
- d.textContent = String(str);
678
- return d.innerHTML;
679
- }
446
+ /* --- Profils de méta-configuration --- */
447
+ let profiles = {};
448
+ let activeProfileId = null;
449
+ let profilesSynced = true;
450
+ let syncInterval = null;
680
451
 
681
- function toggleSidebar() {
682
- const sidebar = document.getElementById('metaSidebar');
683
- const expandBtn = document.getElementById('sidebarExpandBtn');
684
- const collapsed = sidebar.classList.toggle('collapsed');
685
- expandBtn.style.display = collapsed ? '' : 'none';
686
- setTimeout(() => term.fit(), 210);
687
- }
688
-
689
- async function loadSidebarConfigs() {
452
+ async function loadProfiles() {
690
453
  try {
691
454
  const res = await fetch('/proxy/meta-configs');
692
455
  const data = await res.json();
693
- const configs = data.metaConfigs || {};
694
- sbActiveId = data.activeMetaConfig || null;
695
- renderSidebarConfigs(configs, sbActiveId);
696
- if (sbActiveId && !sbSyncInterval) {
697
- sbCheckSync();
698
- sbSyncInterval = setInterval(sbCheckSync, 30000);
699
- }
700
- } catch (err) {
701
- document.getElementById('sidebarConfigs').innerHTML = '<p class="sidebar-empty">Erreur de chargement</p>';
702
- }
703
- }
704
-
705
- function renderSidebarConfigs(configs, activeId) {
706
- const el = document.getElementById('sidebarConfigs');
707
- const entries = Object.entries(configs);
708
- if (!entries.length) {
709
- el.innerHTML = '<p class="sidebar-empty">Aucun profil</p>';
710
- return;
711
- }
712
- el.innerHTML = entries.map(([id, cfg]) => {
713
- const isActive = id === activeId;
714
- return '<div class="sb-config-card' + (isActive ? ' active' : '') + '" data-id="' + sbEsc(id) + '">'
715
- + '<div class="sb-config-row">'
716
- + '<span class="sb-config-name">' + sbEsc(cfg.name) + '</span>'
717
- + '<div class="sb-config-actions">'
718
- + '<button class="sb-icon-btn sb-icon-btn-green" data-id="' + sbEsc(id) + '" onclick="sidebarApply(this.dataset.id, this)" title="Appliquer">&#10003;</button>'
719
- + '<button class="sb-icon-btn sb-icon-btn-ghost" data-id="' + sbEsc(id) + '" onclick="sidebarEdit(this.dataset.id)" title="Modifier">&#9998;</button>'
720
- + '<button class="sb-icon-btn sb-icon-btn-red" data-id="' + sbEsc(id) + '" onclick="sidebarDelete(this.dataset.id)" title="Supprimer">&#10005;</button>'
721
- + '</div></div></div>';
722
- }).join('');
723
- }
724
-
725
- async function sidebarApply(id, btn) {
726
- if (btn.disabled) return;
727
- btn.disabled = true;
728
- btn.innerHTML = '<span class="sb-spinner"></span>';
729
- try {
730
- const res = await fetch('/proxy/meta-configs/' + encodeURIComponent(id) + '/apply', { method: 'POST' });
731
- const data = await res.json();
732
- if (data.success) {
733
- sbActiveId = id;
734
- sbSynced = true;
735
- document.getElementById('sidebarSyncWarning').style.display = 'none';
736
- if (!sbSyncInterval) {
737
- sbCheckSync();
738
- sbSyncInterval = setInterval(sbCheckSync, 30000);
739
- }
740
- await loadSidebarConfigs();
741
- } else {
742
- alert('Erreur lors de l\u2019application du profil.');
743
- }
744
- } catch (err) {
745
- alert('Erreur r\u00e9seau: ' + err.message);
746
- } finally {
747
- btn.disabled = false;
748
- btn.innerHTML = '&#10003;';
749
- }
750
- }
751
-
752
- async function sidebarDelete(id) {
753
- if (!confirm('Supprimer ce profil ?')) return;
754
- try {
755
- const res = await fetch('/proxy/meta-configs/' + encodeURIComponent(id), { method: 'DELETE' });
756
- if (res.ok) {
757
- await loadSidebarConfigs();
758
- } else {
759
- const data = await res.json();
760
- alert('Erreur: ' + (data.error || 'Inconnue'));
761
- }
762
- } catch (err) {
763
- alert('Erreur r\u00e9seau: ' + err.message);
456
+ profiles = data.metaConfigs || {};
457
+ activeProfileId = data.activeMetaConfig || null;
458
+ renderProfileSelect();
459
+ renderProfileMenu();
460
+ startSyncPolling();
461
+ } catch (_) {
462
+ document.getElementById('profileMenu').innerHTML = '<div class="menu-empty">Erreur de chargement</div>';
764
463
  }
765
464
  }
766
465
 
767
- async function sidebarDiscover() {
768
- const btn = document.getElementById('sbDiscoverBtn');
769
- btn.disabled = true;
770
- btn.innerHTML = '<span class="sb-spinner"></span>';
771
- try {
772
- const res = await fetch('/proxy/mcp-configurations');
773
- const data = await res.json();
774
- sbDiscoveredMcps = data.results || [];
775
- const compatible = sbDiscoveredMcps.filter(m => m.configurations !== null);
776
- document.getElementById('sbCreateBtn').disabled = compatible.length === 0;
777
- } catch (err) {
778
- alert('Erreur lors de la d\u00e9couverte: ' + err.message);
779
- } finally {
780
- btn.disabled = false;
781
- btn.innerHTML = '&#128269;';
466
+ function renderProfileSelect() {
467
+ const dot = document.getElementById('profileDot');
468
+ const name = document.getElementById('profileName');
469
+ const active = activeProfileId && profiles[activeProfileId];
470
+ name.textContent = active ? active.name : 'Aucun profil';
471
+ dot.className = 'dot' + (!active ? '' : (profilesSynced ? ' up' : ' warn'));
472
+ }
473
+
474
+ function renderProfileMenu() {
475
+ const menu = document.getElementById('profileMenu');
476
+ const entries = Object.entries(profiles);
477
+ let html = entries.length
478
+ ? entries.map(([id, cfg]) =>
479
+ '<button class="menu-item' + (id === activeProfileId ? ' active' : '') + '" onclick="applyProfile(\'' + esc(id) + '\')">'
480
+ + (id === activeProfileId
481
+ ? '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M20 6 9 17l-5-5"/></svg>'
482
+ : '<span style="width:16px;"></span>')
483
+ + '<span>' + esc(cfg.name) + '</span></button>').join('')
484
+ : '<div class="menu-empty">Aucun profil</div>';
485
+
486
+ html += '<div class="menu-sep"></div>'
487
+ + '<a class="menu-item" href="/proxy/meta-configs-ui">'
488
+ + '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="21" x2="14" y1="4" y2="4"/><line x1="10" x2="3" y1="4" y2="4"/><line x1="21" x2="12" y1="12" y2="12"/><line x1="8" x2="3" y1="12" y2="12"/><line x1="21" x2="16" y1="20" y2="20"/><line x1="12" x2="3" y1="20" y2="20"/><line x1="14" x2="14" y1="2" y2="6"/><line x1="8" x2="8" y1="10" y2="14"/><line x1="16" x2="16" y1="18" y2="22"/></svg>'
489
+ + '<span>Gérer les profils…</span></a>';
490
+
491
+ if (activeProfileId && !profilesSynced) {
492
+ html += '<div class="menu-sep"></div>'
493
+ + '<button class="menu-item" style="color:#FBBF24;" onclick="applyProfile(\'' + esc(activeProfileId) + '\')">'
494
+ + '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="#FBBF24" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>'
495
+ + '<span>Désynchronisé — resynchroniser</span></button>';
782
496
  }
783
- }
784
497
 
785
- function openSbModal() {
786
- document.getElementById('sbModalTitle').textContent = 'Nouveau profil';
787
- document.getElementById('sbModalSubmitBtn').textContent = 'Cr\u00e9er';
788
- document.getElementById('sbEditId').value = '';
789
- document.getElementById('sbCfgId').value = '';
790
- document.getElementById('sbCfgId').readOnly = false;
791
- document.getElementById('sbCfgName').value = '';
792
- document.getElementById('sbCfgDesc').value = '';
793
- sbRenderModalMappings({});
794
- document.getElementById('sbModalOverlay').classList.add('active');
498
+ menu.innerHTML = html;
795
499
  }
796
500
 
797
- async function sidebarEdit(id) {
501
+ async function applyProfile(id) {
502
+ closeAllMenus();
503
+ const name = document.getElementById('profileName');
504
+ const original = name.textContent;
505
+ name.textContent = 'Application…';
798
506
  try {
799
- const res = await fetch('/proxy/meta-configs');
800
- const data = await res.json();
801
- const cfg = data.metaConfigs[id];
802
- if (!cfg) return alert('Profil introuvable');
803
-
804
- document.getElementById('sbModalTitle').textContent = 'Modifier\u00a0: ' + cfg.name;
805
- document.getElementById('sbModalSubmitBtn').textContent = 'Mettre \u00e0 jour';
806
- document.getElementById('sbEditId').value = id;
807
- document.getElementById('sbCfgId').value = id;
808
- document.getElementById('sbCfgId').readOnly = true;
809
- document.getElementById('sbCfgName').value = cfg.name;
810
- document.getElementById('sbCfgDesc').value = cfg.description || '';
811
-
812
- if (sbDiscoveredMcps.length === 0) {
813
- await sidebarDiscover();
814
- }
815
- sbRenderModalMappings(cfg.mappings || {});
816
- document.getElementById('sbModalOverlay').classList.add('active');
817
- } catch (err) {
818
- alert('Erreur: ' + err.message);
819
- }
820
- }
821
-
822
- function sbRenderModalMappings(currentMappings) {
823
- const container = document.getElementById('sbModalMappings');
824
- const compatible = sbDiscoveredMcps.filter(m => m.configurations !== null);
825
- const compatiblePaths = new Set(compatible.map(m => m.path));
826
-
827
- sbOfflineMappings = {};
828
- Object.entries(currentMappings).forEach(([path, configId]) => {
829
- if (!compatiblePaths.has(path)) sbOfflineMappings[path] = configId;
830
- });
831
-
832
- if (compatible.length === 0 && Object.keys(sbOfflineMappings).length === 0) {
833
- container.innerHTML = '<p style="font-size:0.75rem;color:#71717a;">Lancez d\u2019abord la d\u00e9couverte des MCPs.</p>';
834
- return;
835
- }
836
-
837
- const onlineHtml = compatible.map(mcp => {
838
- const selected = currentMappings[mcp.path] || '';
839
- const options = mcp.configurations.map(c =>
840
- '<option value="' + sbEsc(c.id) + '"' + (c.id === selected ? ' selected' : '') + '>'
841
- + sbEsc(c.name || c.id) + (c.active ? ' (actif)' : '') + '</option>'
842
- ).join('');
843
- return '<div class="sb-modal-mapping-item">'
844
- + '<div style="flex:1;"><div class="mcp-label">' + sbEsc(mcp.name) + '</div>'
845
- + '<div class="mcp-path-label">' + sbEsc(mcp.path) + '</div></div>'
846
- + '<select data-mcp-path="' + sbEsc(mcp.path) + '"><option value="">-- ignorer --</option>' + options + '</select>'
847
- + '</div>';
848
- }).join('');
849
-
850
- const offlineHtml = Object.entries(sbOfflineMappings).map(([path, configId]) =>
851
- '<div class="sb-modal-mapping-item" style="opacity:0.5;">'
852
- + '<div style="flex:1;"><div class="mcp-label">&#9888; ' + sbEsc(path) + '</div>'
853
- + '<div class="mcp-path-label">MCP hors ligne &mdash; mapping conserv\u00e9</div></div>'
854
- + '<span style="font-size:0.8rem;color:#a1a1aa;">' + sbEsc(configId) + '</span>'
855
- + '</div>'
856
- ).join('');
857
-
858
- container.innerHTML = onlineHtml + offlineHtml;
859
- }
860
-
861
- async function sbHandleFormSubmit(e) {
862
- e.preventDefault();
863
- const editId = document.getElementById('sbEditId').value;
864
- const id = document.getElementById('sbCfgId').value.trim();
865
- const name = document.getElementById('sbCfgName').value.trim();
866
- const description = document.getElementById('sbCfgDesc').value.trim();
867
-
868
- const mappings = { ...sbOfflineMappings };
869
- document.querySelectorAll('#sbModalMappings select[data-mcp-path]').forEach(sel => {
870
- if (sel.value) mappings[sel.dataset.mcpPath] = sel.value;
871
- });
872
-
873
- const btn = document.getElementById('sbModalSubmitBtn');
874
- btn.disabled = true;
875
- try {
876
- let res;
877
- if (editId) {
878
- res = await fetch('/proxy/meta-configs/' + encodeURIComponent(editId), {
879
- method: 'PUT',
880
- headers: { 'Content-Type': 'application/json' },
881
- body: JSON.stringify({ name, description, mappings })
882
- });
883
- } else {
884
- res = await fetch('/proxy/meta-configs', {
885
- method: 'POST',
886
- headers: { 'Content-Type': 'application/json' },
887
- body: JSON.stringify({ id, name, description, mappings })
888
- });
889
- }
507
+ const res = await fetch('/proxy/meta-configs/' + encodeURIComponent(id) + '/apply', { method: 'POST' });
890
508
  const data = await res.json();
891
- if (res.ok) {
892
- closeSbModal();
893
- await loadSidebarConfigs();
509
+ if (data.success) {
510
+ activeProfileId = id;
511
+ profilesSynced = true;
512
+ renderProfileSelect();
513
+ renderProfileMenu();
514
+ startSyncPolling();
894
515
  } else {
895
- alert('Erreur: ' + (data.error || 'Inconnue'));
516
+ name.textContent = original;
517
+ alert('Erreur lors de l’application du profil.');
896
518
  }
897
519
  } catch (err) {
898
- alert('Erreur r\u00e9seau: ' + err.message);
899
- } finally {
900
- btn.disabled = false;
520
+ name.textContent = original;
521
+ alert('Erreur réseau: ' + err.message);
901
522
  }
902
523
  }
903
524
 
904
- function closeSbModal() {
905
- document.getElementById('sbModalOverlay').classList.remove('active');
906
- }
907
-
908
- async function sbCheckSync() {
525
+ async function checkSync() {
909
526
  try {
910
527
  const res = await fetch('/proxy/meta-configs/active/sync');
911
528
  const data = await res.json();
912
- const wasSynced = sbSynced;
913
- sbSynced = data.synced !== false;
914
- const warning = document.getElementById('sidebarSyncWarning');
915
- if (!sbSynced) {
916
- warning.style.display = '';
917
- const activeCard = document.querySelector('#sidebarConfigs .sb-config-card.active');
918
- if (activeCard) activeCard.classList.remove('active');
919
- } else if (!wasSynced && sbSynced) {
920
- warning.style.display = 'none';
921
- await loadSidebarConfigs();
922
- }
529
+ const was = profilesSynced;
530
+ profilesSynced = data.synced !== false;
531
+ if (was !== profilesSynced) { renderProfileSelect(); renderProfileMenu(); }
923
532
  } catch (_) {}
924
533
  }
925
534
 
926
- async function loadSidebarRoutes() {
927
- try {
928
- const res = await fetch('/proxy/routes');
929
- const data = await res.json();
930
- renderSidebarRoutes(data.routes || []);
931
- } catch (_) {}
535
+ function startSyncPolling() {
536
+ if (syncInterval || !activeProfileId) return;
537
+ checkSync();
538
+ syncInterval = setInterval(checkSync, 30000);
932
539
  }
933
540
 
934
- function renderSidebarRoutes(routes) {
935
- const el = document.getElementById('sidebarServices');
936
- if (!routes.length) {
937
- el.innerHTML = '<p class="sidebar-empty">Aucun MCP connect\u00e9</p>';
938
- return;
939
- }
940
- el.innerHTML = routes.map(r =>
941
- '<div class="sb-service-card" data-path="' + sbEsc(r.path) + '" onclick="sidebarOpenRoute(this.dataset.path)">'
942
- + '<span class="sb-service-name" title="' + sbEsc(r.name || r.path) + '">' + sbEsc(r.name || r.path) + '</span>'
943
- + '<div class="sb-service-actions">'
944
- + '<button class="sb-icon-btn sb-icon-btn-purple" data-path="' + sbEsc(r.path) + '" onclick="event.stopPropagation(); sidebarOpenRoute(this.dataset.path)" title="Ouvrir dans une nouvelle fen&#234;tre">&#x26F6;</button>'
945
- + '<button class="sb-icon-btn sb-icon-btn-blue" data-path="' + sbEsc(r.path) + '" onclick="event.stopPropagation(); sidebarOpenRoute(this.dataset.path)" title="Ouvrir dans une nouvelle fen&#234;tre">&#8599;</button>'
946
- + '<button class="sb-icon-btn sb-icon-btn-amber" data-path="' + sbEsc(r.path) + '" onclick="event.stopPropagation(); sidebarCheckHealth(this.dataset.path, this)" title="Sant&#233;">&#10084;</button>'
947
- + '</div></div>'
948
- ).join('');
949
- }
541
+ window.addEventListener('beforeunload', () => { if (syncInterval) clearInterval(syncInterval); });
542
+
543
+ /* --- Services enregistrés --- */
544
+ let servicesLoaded = false;
950
545
 
951
- function sidebarOpenRoute(path) {
952
- window.open(path, '_blank');
546
+ function toggleServicesMenu(btn) {
547
+ toggleMenu(btn);
548
+ if (!servicesLoaded) loadServices();
953
549
  }
954
550
 
955
- async function sidebarCheckHealth(path, btn) {
956
- btn.disabled = true;
957
- btn.innerHTML = '<span class="sb-spinner"></span>';
551
+ async function loadServices() {
958
552
  try {
959
- const res = await fetch('/proxy/check', {
960
- method: 'POST',
961
- headers: { 'Content-Type': 'application/json' },
962
- body: JSON.stringify({ path })
963
- });
553
+ const res = await fetch('/proxy/routes');
964
554
  const data = await res.json();
965
- btn.innerHTML = data.healthy ? '&#10003;' : '&#10005;';
966
- btn.className = 'sb-icon-btn ' + (data.healthy ? 'sb-icon-btn-green' : 'sb-icon-btn-red');
967
- if (!data.healthy) await loadSidebarRoutes();
968
- else setTimeout(() => { btn.innerHTML = '&#10084;'; btn.className = 'sb-icon-btn sb-icon-btn-amber'; btn.disabled = false; }, 2000);
555
+ const routes = data.routes || [];
556
+ const menu = document.getElementById('servicesMenu');
557
+ menu.innerHTML = routes.length
558
+ ? routes.map(r =>
559
+ '<a class="menu-item" href="' + esc(r.path) + '" target="_blank" rel="noopener noreferrer">'
560
+ + '<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M15 3h6v6"/><path d="M10 14 21 3"/><path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"/></svg>'
561
+ + '<span>' + esc(r.name || r.path) + '</span></a>').join('')
562
+ : '<div class="menu-empty">Aucun service enregistré</div>';
563
+ servicesLoaded = true;
969
564
  } catch (_) {
970
- btn.innerHTML = '&#10084;';
971
- btn.disabled = false;
972
- }
973
- }
974
-
975
- async function refreshSidebarRoutes() {
976
- const btn = document.getElementById('sbRefreshRoutesBtn');
977
- btn.disabled = true;
978
- btn.classList.add('spinning');
979
- try {
980
- await loadSidebarRoutes();
981
- } finally {
982
- btn.disabled = false;
983
- btn.classList.remove('spinning');
565
+ document.getElementById('servicesMenu').innerHTML = '<div class="menu-empty">Erreur de chargement</div>';
984
566
  }
985
567
  }
986
568
 
987
- loadSidebarConfigs();
988
- loadSidebarRoutes();
569
+ loadProfiles();
989
570
  </script>
990
571
  </body>
991
572
  </html>