@imenam/mcp-github 1.1.47 → 1.1.49

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/public/index.html CHANGED
@@ -6,17 +6,37 @@
6
6
  <title>GitHub MCP</title>
7
7
  <script src="https://cdn.tailwindcss.com"></script>
8
8
  <style>
9
+ :root {
10
+ --bg: #0a0a0b;
11
+ --surface: #141416;
12
+ --surface-2: #1b1b1e;
13
+ --border: #2a2a2e;
14
+ --border-strong: #3a3a3f;
15
+ --text: #ededef;
16
+ --text-dim: #9a9aa2;
17
+ --text-muted: #6b6b73;
18
+ --accent: #4d7cfe;
19
+ --accent-hover: #6a92ff;
20
+ --danger: #e0635e;
21
+ --radius: 8px;
22
+ }
23
+
24
+ * {
25
+ -webkit-font-smoothing: antialiased;
26
+ }
27
+
9
28
  body {
10
- background-color: #0f172a;
11
- color: #f8fafc;
29
+ background-color: var(--bg);
30
+ color: var(--text);
31
+ font-family: ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
12
32
  }
13
33
 
14
34
  /* Layout */
15
35
  .app-container {
16
36
  display: grid;
17
- grid-template-columns: 320px 1fr;
18
- gap: 2rem;
19
- max-width: 1200px;
37
+ grid-template-columns: 300px 1fr;
38
+ gap: 1.5rem;
39
+ max-width: 1080px;
20
40
  margin: 0 auto;
21
41
  }
22
42
 
@@ -26,179 +46,337 @@
26
46
  }
27
47
  }
28
48
 
29
- /* iPhone style slider */
30
- .switch {
31
- position: relative;
32
- display: inline-block;
33
- width: 44px;
34
- height: 24px;
49
+ /* Surfaces */
50
+ .card {
51
+ background-color: var(--surface);
52
+ border: 1px solid var(--border);
53
+ border-radius: var(--radius);
54
+ }
55
+
56
+ .section-label {
57
+ font-size: 0.6875rem;
58
+ font-weight: 600;
59
+ letter-spacing: 0.08em;
60
+ text-transform: uppercase;
61
+ color: var(--text-muted);
62
+ }
63
+
64
+ .divider {
65
+ border-top: 1px solid var(--border);
66
+ }
67
+
68
+ /* Text helpers */
69
+ .text-main { color: var(--text); }
70
+ .text-dim { color: var(--text-dim); }
71
+ .text-muted { color: var(--text-muted); }
72
+
73
+ /* Buttons */
74
+ .btn {
75
+ display: flex;
76
+ align-items: center;
77
+ justify-content: center;
78
+ gap: 0.5rem;
79
+ width: 100%;
80
+ padding: 0.55rem 0.9rem;
81
+ font-size: 0.875rem;
82
+ font-weight: 500;
83
+ border-radius: var(--radius);
84
+ border: 1px solid transparent;
85
+ cursor: pointer;
86
+ transition: background-color .15s ease, border-color .15s ease, color .15s ease, opacity .15s ease;
87
+ }
88
+ .btn:disabled {
89
+ opacity: 0.4;
90
+ cursor: not-allowed;
91
+ }
92
+
93
+ .btn-primary {
94
+ background-color: var(--accent);
95
+ color: #fff;
96
+ }
97
+ .btn-primary:not(:disabled):hover { background-color: var(--accent-hover); }
98
+
99
+ .btn-secondary {
100
+ background-color: transparent;
101
+ border-color: var(--border);
102
+ color: var(--text-dim);
103
+ }
104
+ .btn-secondary:not(:disabled):hover {
105
+ background-color: var(--surface-2);
106
+ border-color: var(--border-strong);
107
+ color: var(--text);
108
+ }
109
+
110
+ .btn-danger {
111
+ background-color: transparent;
112
+ border-color: var(--border);
113
+ color: var(--danger);
114
+ }
115
+ .btn-danger:not(:disabled):hover {
116
+ background-color: rgba(224, 99, 94, 0.08);
117
+ border-color: var(--danger);
118
+ }
119
+
120
+ .btn-icon {
121
+ display: inline-flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ width: 2.25rem;
125
+ padding: 0;
35
126
  flex-shrink: 0;
36
127
  }
37
128
 
38
- .switch input {
39
- opacity: 0;
40
- width: 0;
41
- height: 0;
129
+ /* Form fields */
130
+ .field-label {
131
+ display: block;
132
+ font-size: 0.75rem;
133
+ font-weight: 500;
134
+ color: var(--text-dim);
135
+ margin-bottom: 0.4rem;
136
+ }
137
+
138
+ .field-input,
139
+ .field-select {
140
+ width: 100%;
141
+ padding: 0.5rem 0.75rem;
142
+ font-size: 0.875rem;
143
+ background-color: var(--bg);
144
+ border: 1px solid var(--border);
145
+ border-radius: var(--radius);
146
+ color: var(--text);
147
+ outline: none;
148
+ transition: border-color .15s ease;
149
+ }
150
+ .field-input::placeholder { color: var(--text-muted); }
151
+ .field-input:focus,
152
+ .field-select:focus { border-color: var(--accent); }
153
+ .field-select { cursor: pointer; }
154
+
155
+ /* Toggle rows */
156
+ .toggle-row {
157
+ display: flex;
158
+ align-items: center;
159
+ justify-content: space-between;
160
+ gap: 1rem;
161
+ padding: 0.65rem 0;
162
+ }
163
+ .toggle-row + .toggle-row { border-top: 1px solid var(--border); }
164
+ .toggle-row label.text {
165
+ font-size: 0.875rem;
166
+ color: var(--text-dim);
167
+ }
168
+
169
+ .info-row {
170
+ display: flex;
171
+ align-items: center;
172
+ justify-content: space-between;
173
+ gap: 1rem;
174
+ padding: 0.65rem 0.75rem;
175
+ background-color: var(--bg);
176
+ border: 1px solid var(--border);
177
+ border-radius: var(--radius);
178
+ }
179
+
180
+ /* Collapsible form sections */
181
+ .form-section {
182
+ border: 1px solid var(--border);
183
+ border-radius: var(--radius);
184
+ overflow: hidden;
42
185
  }
186
+ .section-header {
187
+ display: flex;
188
+ align-items: center;
189
+ justify-content: space-between;
190
+ width: 100%;
191
+ padding: 0.7rem 0.9rem;
192
+ background-color: var(--surface-2);
193
+ border: none;
194
+ cursor: pointer;
195
+ font-size: 0.8125rem;
196
+ font-weight: 600;
197
+ letter-spacing: 0.02em;
198
+ color: var(--text);
199
+ transition: background-color .15s ease;
200
+ }
201
+ .section-header:hover { background-color: var(--border); }
202
+ .section-header .chevron { transition: transform .2s ease; color: var(--text-muted); }
203
+ .section-header.open .chevron { transform: rotate(90deg); }
204
+ .section-static-header {
205
+ padding: 0.7rem 0.9rem;
206
+ background-color: var(--surface-2);
207
+ font-size: 0.8125rem;
208
+ font-weight: 600;
209
+ letter-spacing: 0.02em;
210
+ color: var(--text);
211
+ }
212
+ .section-body { padding: 1rem 0.9rem; }
43
213
 
214
+ /* iPhone style slider (sober) */
215
+ .switch {
216
+ position: relative;
217
+ display: inline-block;
218
+ width: 40px;
219
+ height: 22px;
220
+ flex-shrink: 0;
221
+ }
222
+ .switch input { opacity: 0; width: 0; height: 0; }
44
223
  .slider {
45
224
  position: absolute;
46
225
  cursor: pointer;
47
- top: 0;
48
- left: 0;
49
- right: 0;
50
- bottom: 0;
51
- background-color: #334155;
52
- transition: .4s;
53
- border-radius: 24px;
226
+ inset: 0;
227
+ background-color: var(--border-strong);
228
+ transition: .25s;
229
+ border-radius: 22px;
54
230
  }
55
-
56
231
  .slider:before {
57
232
  position: absolute;
58
233
  content: "";
59
- height: 18px;
60
- width: 18px;
234
+ height: 16px;
235
+ width: 16px;
61
236
  left: 3px;
62
237
  bottom: 3px;
63
- background-color: white;
64
- transition: .4s;
238
+ background-color: #fff;
239
+ transition: .25s;
65
240
  border-radius: 50%;
66
241
  }
242
+ input:checked + .slider { background-color: var(--accent); }
243
+ input:checked + .slider:before { transform: translateX(18px); }
67
244
 
68
- input:checked + .slider {
69
- background-color: #2563eb;
70
- }
71
-
72
- input:checked + .slider:before {
73
- transform: translateX(20px);
74
- }
75
-
76
- /* Custom dropdown styles */
245
+ /* Custom dropdown */
77
246
  .search-results {
78
247
  position: absolute;
79
248
  top: 100%;
80
249
  left: 0;
81
250
  right: 0;
82
251
  z-index: 50;
83
- background-color: #0f172a;
84
- border: 1px solid #334155;
85
- border-radius: 0.5rem;
252
+ background-color: var(--surface-2);
253
+ border: 1px solid var(--border);
254
+ border-radius: var(--radius);
86
255
  margin-top: 0.25rem;
87
256
  max-height: 15rem;
88
257
  overflow-y: auto;
89
- box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05);
258
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
90
259
  }
91
-
92
260
  .search-result-item {
93
- padding: 0.5rem 1rem;
261
+ padding: 0.5rem 0.75rem;
94
262
  cursor: pointer;
95
- transition: background-color 0.2s;
263
+ transition: background-color .12s;
96
264
  font-size: 0.875rem;
265
+ color: var(--text-dim);
97
266
  }
98
-
99
267
  .search-result-item:hover {
100
- background-color: #1e293b;
101
- color: #60a5fa;
268
+ background-color: var(--surface);
269
+ color: var(--text);
102
270
  }
103
-
104
271
  .search-result-item.active {
105
- background-color: #1e293b;
106
- color: #60a5fa;
107
- outline: 1px solid #3b82f6;
272
+ background-color: var(--surface);
273
+ color: var(--text);
108
274
  }
109
-
110
275
  .search-result-item.selected {
111
- background-color: #2563eb;
112
- color: white;
276
+ background-color: rgba(77, 124, 254, 0.12);
277
+ color: var(--accent-hover);
278
+ }
279
+
280
+ /* Badge */
281
+ .badge {
282
+ display: inline-flex;
283
+ align-items: center;
284
+ padding: 0.15rem 0.6rem;
285
+ font-size: 0.75rem;
286
+ font-weight: 500;
287
+ color: var(--text-dim);
288
+ background-color: var(--surface-2);
289
+ border: 1px solid var(--border);
290
+ border-radius: 999px;
291
+ }
292
+
293
+ /* Spinner */
294
+ .spinner {
295
+ display: inline-block;
296
+ width: 1rem;
297
+ height: 1rem;
298
+ border: 2px solid var(--border-strong);
299
+ border-top-color: var(--accent);
300
+ border-radius: 50%;
301
+ animation: spin 0.7s linear infinite;
113
302
  }
303
+ @keyframes spin { to { transform: rotate(360deg); } }
114
304
 
115
305
  /* Toasts */
116
306
  .toast {
117
- animation: slideIn 0.3s ease-out forwards, fadeOut 0.3s ease-in forwards 4.7s;
118
- max-width: 24rem;
307
+ display: flex;
308
+ align-items: center;
309
+ gap: 0.6rem;
310
+ padding: 0.7rem 0.9rem;
311
+ font-size: 0.875rem;
312
+ background-color: var(--surface-2);
313
+ border: 1px solid var(--border);
314
+ border-left-width: 3px;
315
+ border-radius: var(--radius);
316
+ color: var(--text);
317
+ max-width: 22rem;
318
+ box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
319
+ animation: slideIn 0.25s ease-out forwards, fadeOut 0.3s ease-in forwards 4.7s;
119
320
  }
321
+ .toast-success { border-left-color: var(--accent); }
322
+ .toast-error { border-left-color: var(--danger); }
120
323
 
121
324
  @keyframes slideIn {
122
- from { transform: translateX(100%); opacity: 0; }
325
+ from { transform: translateX(110%); opacity: 0; }
123
326
  to { transform: translateX(0); opacity: 1; }
124
327
  }
125
-
126
328
  @keyframes fadeOut {
127
329
  from { opacity: 1; }
128
330
  to { opacity: 0; }
129
331
  }
130
332
  </style>
131
333
  </head>
132
- <body class="min-h-screen p-8">
334
+ <body class="min-h-screen p-6 md:p-10">
133
335
  <div class="app-container">
134
336
  <!-- Sidebar -->
135
- <aside class="space-y-6">
136
- <div class="bg-slate-800 rounded-xl shadow-2xl p-6 border border-slate-700 sticky top-8">
137
- <h2 class="text-xl font-bold mb-4 text-blue-400 flex items-center">
138
- <svg class="w-6 h-6 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
139
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10"></path>
140
- </svg>
141
- Configuration
142
- </h2>
337
+ <aside>
338
+ <div class="card p-5 sticky top-10">
339
+ <h2 class="section-label mb-4">Configuration</h2>
143
340
 
144
341
  <!-- Configuration Selector -->
145
- <div class="mb-6 space-y-2">
146
- <div class="flex gap-2">
147
- <select id="configSelector" class="flex-1 px-3 py-2 bg-slate-900 border border-slate-600 rounded-lg text-sm focus:ring-2 focus:ring-blue-500 outline-none">
148
- <option value="">-- Sélectionner --</option>
149
- </select>
150
- <button type="button" id="newConfigBtn" class="px-3 py-2 bg-green-600 hover:bg-green-700 rounded-lg transition duration-200" title="Nouvelle configuration">
151
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
152
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
153
- </svg>
154
- </button>
155
- <button type="button" id="deleteConfigBtn" class="px-3 py-2 bg-red-600 hover:bg-red-700 disabled:bg-slate-700 disabled:cursor-not-allowed rounded-lg transition duration-200" title="Supprimer la configuration" disabled>
156
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
157
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
158
- </svg>
159
- </button>
160
- </div>
161
- </div>
162
-
163
- <div class="border-t border-slate-700 pt-4 mb-4">
164
- <h3 class="text-sm font-medium text-slate-400 mb-3">Actions</h3>
165
- </div>
166
-
167
- <div class="space-y-4">
168
- <button type="button" onclick="document.getElementById('configForm').requestSubmit()" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
169
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
170
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
342
+ <div class="flex gap-2 mb-5">
343
+ <select id="configSelector" class="field-select flex-1">
344
+ <option value="">— Sélectionner —</option>
345
+ </select>
346
+ <button type="button" id="newConfigBtn" class="btn btn-secondary btn-icon" title="Nouvelle configuration">
347
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
348
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"></path>
171
349
  </svg>
172
- <span>Sauvegarder</span>
173
350
  </button>
174
-
175
- <button type="button" id="cloneBtn" class="w-full bg-indigo-600 hover:bg-indigo-700 disabled:bg-slate-700 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
176
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
177
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V9a2 2 0 00-2-2h-3m-1 4l-3 3m0 0l-3-3m3 3V4"></path>
351
+ <button type="button" id="deleteConfigBtn" class="btn btn-danger btn-icon" title="Supprimer la configuration" disabled>
352
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
353
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
178
354
  </svg>
179
- <span>Cloner le dépôt</span>
180
355
  </button>
356
+ </div>
181
357
 
182
- <button type="button" id="restartBtn" class="w-full bg-amber-600 hover:bg-amber-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
183
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
184
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"></path>
185
- </svg>
186
- <span>Redémarrer</span>
187
- </button>
358
+ <div class="divider pt-4">
359
+ <h3 class="section-label mb-3">Actions</h3>
188
360
 
189
- <button type="button" id="deleteWorkingDirBtn" class="w-full bg-orange-600 hover:bg-orange-700 disabled:bg-slate-700 disabled:cursor-not-allowed text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
190
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
191
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path>
192
- </svg>
193
- <span>Nettoyer Repo</span>
194
- </button>
361
+ <div class="space-y-2.5">
362
+ <button type="button" onclick="document.getElementById('configForm').requestSubmit()" class="btn btn-primary">
363
+ Sauvegarder
364
+ </button>
195
365
 
196
- <div class="border-t border-slate-700 pt-4">
197
- <button type="button" id="stopBtn" class="w-full bg-red-600 hover:bg-red-700 text-white font-bold py-3 px-4 rounded-lg transition duration-200 shadow-lg flex justify-center items-center">
198
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
199
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
200
- </svg>
201
- <span>Arrêter</span>
366
+ <button type="button" id="cloneBtn" class="btn btn-secondary">
367
+ Cloner le dépôt
368
+ </button>
369
+
370
+ <button type="button" id="restartBtn" class="btn btn-secondary">
371
+ Redémarrer
372
+ </button>
373
+
374
+ <button type="button" id="deleteWorkingDirBtn" class="btn btn-danger">
375
+ Nettoyer le repo
376
+ </button>
377
+
378
+ <button type="button" id="stopBtn" class="btn btn-danger">
379
+ Arrêter le serveur
202
380
  </button>
203
381
  </div>
204
382
  </div>
@@ -206,44 +384,44 @@
206
384
  </aside>
207
385
 
208
386
  <!-- Main Content -->
209
- <main class="bg-slate-800 rounded-xl shadow-2xl p-8 border border-slate-700">
210
- <div class="flex items-center gap-4 mb-6">
211
- <h1 class="text-3xl font-bold text-blue-400 flex items-center">
212
- <svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
213
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"></path>
387
+ <main class="card p-7">
388
+ <div class="flex items-center gap-3 mb-7">
389
+ <h1 class="text-xl font-semibold text-main flex items-center gap-2.5">
390
+ <svg class="w-5 h-5 text-dim" fill="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
391
+ <path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"></path>
214
392
  </svg>
215
393
  GitHub MCP
216
394
  </h1>
217
- <span id="agentNameBadge" class="hidden px-3 py-1 bg-blue-900/50 text-blue-300 text-sm font-medium rounded-full border border-blue-700"></span>
395
+ <span id="agentNameBadge" class="badge hidden"></span>
218
396
  </div>
219
-
220
- <form id="configForm" class="space-y-6">
397
+
398
+ <form id="configForm" class="space-y-5">
221
399
  <div class="space-y-4" id="inputs">
222
400
  <!-- Inputs will be injected here -->
223
401
  <div class="flex justify-center p-8">
224
- <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400"></div>
402
+ <div class="spinner"></div>
225
403
  </div>
226
404
  </div>
227
405
  </form>
228
406
 
229
- <div class="mt-8 text-sm text-slate-400 border-t border-slate-700 pt-4 italic">
230
- Note: Le serveur MCP doit être redémarré pour appliquer les changements.
407
+ <div class="mt-8 divider pt-4 text-xs text-muted">
408
+ Le serveur MCP doit être redémarré pour appliquer les changements.
231
409
  </div>
232
410
  </main>
233
411
  </div>
234
412
 
235
413
  <!-- Toast Container -->
236
- <div id="toast-container" class="fixed bottom-8 right-8 z-50 flex flex-col gap-3"></div>
414
+ <div id="toast-container" class="fixed bottom-6 right-6 z-50 flex flex-col gap-2.5"></div>
237
415
 
238
416
  <script>
239
417
  // Détection automatique du base path pour fonctionner via proxy
240
418
  const pathname = window.location.pathname;
241
- const BASE_PATH = pathname.includes('.')
419
+ const BASE_PATH = pathname.includes('.')
242
420
  ? pathname.replace(/\/[^/]*$/, '') // /github-mcp/index.html -> /github-mcp
243
- : pathname.endsWith('/')
421
+ : pathname.endsWith('/')
244
422
  ? pathname.slice(0, -1) // /github-mcp/ -> /github-mcp
245
423
  : pathname === '/' ? '' : pathname; // /github-mcp -> /github-mcp, / -> ''
246
-
424
+
247
425
  // Helper pour construire les URLs d'API
248
426
  const api = (endpoint) => `${BASE_PATH}/${endpoint}`.replace(/\/+/g, '/');
249
427
 
@@ -258,13 +436,50 @@
258
436
  { id: 'TARGET_REPO', label: 'Target Repository', type: 'select', placeholder: 'owner/repo' },
259
437
  { id: 'BASE_BRANCH', label: 'Base Branch', type: 'select', placeholder: 'main' },
260
438
  { id: 'TARGET_BRANCH', label: 'Target Branch', type: 'select', placeholder: 'feature-branch' },
261
- { id: 'SSH_REMOTE_PATH', label: 'Chemin distant (SSH_REMOTE_PATH)', type: 'text', placeholder: '/home/user/mon-projet' },
439
+ { id: 'LOCAL_DEPLOY', label: 'Déployer en local (même serveur, sans SSH)', type: 'slider' },
440
+ { id: 'SSH_HOST', label: 'Serveur SSH (SSH_HOST)', type: 'text', placeholder: 'srv.example.com' },
441
+ { id: 'SSH_USER', label: 'Utilisateur SSH (SSH_USER)', type: 'text', placeholder: 'root' },
442
+ { id: 'SSH_PORT', label: 'Port SSH (SSH_PORT)', type: 'text', placeholder: '22' },
443
+ { id: 'SSH_KEY_PATH', label: 'Clé privée SSH (SSH_KEY_PATH)', type: 'text', placeholder: 'C:\\Users\\moi\\.ssh\\id_ed25519' },
444
+ { id: 'SSH_REMOTE_PATH', label: 'Chemin du projet (distant ou local)', type: 'text', placeholder: '/var/www/mon-projet' },
262
445
  { id: 'TEST_COMMAND', label: 'Commande de test (TEST_COMMAND)', type: 'text', placeholder: 'npm test' },
263
446
  { id: 'READ_ONLY', label: 'Mode Lecture Seule', type: 'slider' },
264
447
  { id: 'INCLUDE_PUBLIC', label: 'Inclure les dépôts publics', type: 'slider' },
265
448
  { id: 'INCLUDE_NOT_OWNED', label: 'Inclure les dépôts dont je ne suis pas propriétaire', type: 'slider' }
266
449
  ];
267
450
 
451
+ // Index des champs par id (pour le rendu groupé en sections)
452
+ const fieldsById = Object.fromEntries(fields.map(f => [f.id, f]));
453
+
454
+ // Sections du formulaire. Le coeur (dépôt/branches) est toujours déplié ;
455
+ // les autres sont repliables, le paramétrage serveur étant fait une seule fois.
456
+ const sections = [
457
+ {
458
+ id: 'core',
459
+ title: 'Dépôt & branches',
460
+ collapsible: false,
461
+ fields: ['TARGET_REPO', 'BASE_BRANCH', 'TARGET_BRANCH']
462
+ },
463
+ {
464
+ id: 'options',
465
+ title: 'Options',
466
+ collapsible: true,
467
+ fields: ['READ_ONLY', 'INCLUDE_PUBLIC', 'INCLUDE_NOT_OWNED']
468
+ },
469
+ {
470
+ id: 'deploy',
471
+ title: 'Déploiement & test',
472
+ collapsible: true,
473
+ fields: ['LOCAL_DEPLOY', 'SSH_HOST', 'SSH_USER', 'SSH_PORT', 'SSH_KEY_PATH', 'SSH_REMOTE_PATH', 'TEST_COMMAND']
474
+ }
475
+ ];
476
+
477
+ // État déplié/replié des sections repliables (false = replié par défaut)
478
+ const sectionState = { options: false, deploy: false };
479
+
480
+ // Champs propres à la connexion SSH : masqués quand LOCAL_DEPLOY est actif
481
+ const sshOnlyFields = ['SSH_HOST', 'SSH_USER', 'SSH_PORT', 'SSH_KEY_PATH'];
482
+
268
483
  // État de l'application
269
484
  let currentConfig = {};
270
485
  let currentConfigName = '';
@@ -299,11 +514,11 @@
299
514
  }
300
515
 
301
516
  function renderConfigSelector() {
302
- configSelector.innerHTML = '<option value="">-- Sélectionner --</option>' +
303
- configNames.map(name =>
517
+ configSelector.innerHTML = '<option value="">— Sélectionner —</option>' +
518
+ configNames.map(name =>
304
519
  `<option value="${name}" ${name === currentConfigName ? 'selected' : ''}>${name}</option>`
305
520
  ).join('');
306
-
521
+
307
522
  deleteConfigBtn.disabled = !currentConfigName || configNames.length <= 1;
308
523
  }
309
524
 
@@ -341,6 +556,13 @@
341
556
  TARGET_REPO: '',
342
557
  TARGET_BRANCH: '',
343
558
  BASE_BRANCH: '',
559
+ LOCAL_DEPLOY: false,
560
+ SSH_HOST: '',
561
+ SSH_USER: '',
562
+ SSH_PORT: '',
563
+ SSH_KEY_PATH: '',
564
+ SSH_REMOTE_PATH: '',
565
+ TEST_COMMAND: '',
344
566
  READ_ONLY: currentConfig.READ_ONLY || false,
345
567
  INCLUDE_PUBLIC: currentConfig.INCLUDE_PUBLIC || false,
346
568
  INCLUDE_NOT_OWNED: currentConfig.INCLUDE_NOT_OWNED || false
@@ -409,7 +631,7 @@
409
631
  async function fetchRepositories(searchTerm = '') {
410
632
  const loadingIndicator = document.getElementById('REPO_LOADING_INDICATOR');
411
633
  if (loadingIndicator) loadingIndicator.classList.remove('hidden');
412
-
634
+
413
635
  try {
414
636
  const url = api(`api/repositories${searchTerm ? `?search=${encodeURIComponent(searchTerm)}` : ''}`);
415
637
  const response = await fetch(url);
@@ -434,13 +656,13 @@
434
656
  resultsContainer.innerHTML = selectState.repos.map((repo, index) => `
435
657
  <div class="search-result-item ${repo === currentRepo ? 'selected' : ''}" data-index="${index}" onclick="selectRepo('${repo}')">
436
658
  ${repo}
437
- ${repo === currentRepo ? '<span class="float-right text-blue-400">✓</span>' : ''}
659
+ ${repo === currentRepo ? '<span class="float-right">✓</span>' : ''}
438
660
  </div>
439
661
  `).join('');
440
662
  resultsContainer.classList.remove('hidden');
441
663
  activeIndex = -1;
442
664
  } else {
443
- resultsContainer.innerHTML = '<div class="p-3 text-slate-500 text-sm">Aucun résultat</div>';
665
+ resultsContainer.innerHTML = '<div class="search-result-item">Aucun résultat</div>';
444
666
  resultsContainer.classList.remove('hidden');
445
667
  }
446
668
  }
@@ -450,18 +672,18 @@
450
672
  const searchInput = document.getElementById('REPO_SEARCH');
451
673
  const hiddenInput = document.getElementById('TARGET_REPO');
452
674
  const resultsContainer = document.getElementById('REPO_RESULTS');
453
-
675
+
454
676
  if (searchInput) searchInput.value = repo;
455
677
  if (hiddenInput) {
456
678
  hiddenInput.value = repo;
457
679
  currentConfig.TARGET_REPO = repo;
458
680
  }
459
681
  if (resultsContainer) resultsContainer.classList.add('hidden');
460
-
682
+
461
683
  // Réinitialiser les branches lors du changement de repository
462
684
  currentConfig.BASE_BRANCH = '';
463
685
  currentConfig.TARGET_BRANCH = '';
464
-
686
+
465
687
  // Charger les branches pour ce repo (renderInputs is called inside fetchBranches)
466
688
  await fetchBranches(repo);
467
689
  };
@@ -469,11 +691,11 @@
469
691
  // Fetch branches for a repository
470
692
  async function fetchBranches(repo, preserveConfig = false) {
471
693
  if (!repo) return [];
472
-
694
+
473
695
  selectState.loadingBranches[repo] = true;
474
696
  // Use renderInputs directly to avoid overwriting currentConfig from DOM
475
697
  renderInputs(currentConfig);
476
-
698
+
477
699
  try {
478
700
  const url = api(`api/branches?repo=${encodeURIComponent(repo)}`);
479
701
  const response = await fetch(url);
@@ -492,42 +714,42 @@
492
714
 
493
715
  // Render select field
494
716
  function renderSelectField(field, value, config) {
495
- let options = '<option value="">-- Sélectionner une option --</option>';
496
-
717
+ let options = '<option value="">— Sélectionner une option —</option>';
718
+
497
719
  if (field.id === 'TARGET_REPO') {
498
720
  if (selectState.loadingRepos && selectState.repos.length === 0) {
499
721
  return `
500
- <div class="space-y-1">
501
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
502
- <div id="${field.id}_loading" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg text-slate-400 flex items-center">
503
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
722
+ <div>
723
+ <label for="${field.id}" class="field-label">${field.label}</label>
724
+ <div id="${field.id}_loading" class="field-input flex items-center gap-2 text-dim">
725
+ <div class="spinner"></div>
504
726
  Chargement des dépôts...
505
727
  </div>
506
728
  </div>
507
729
  `;
508
730
  }
509
-
510
- options += selectState.repos.map(repo =>
731
+
732
+ options += selectState.repos.map(repo =>
511
733
  `<option value="${repo}" ${value === repo ? 'selected' : ''}>${repo}</option>`
512
734
  ).join('');
513
-
735
+
514
736
  if (value && !selectState.repos.includes(value)) {
515
737
  options += `<option value="${value}" selected>${value}</option>`;
516
738
  }
517
739
 
518
740
  return `
519
- <div class="space-y-1">
520
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
741
+ <div>
742
+ <label for="${field.id}" class="field-label">${field.label}</label>
521
743
  <div class="relative">
522
744
  <div class="relative">
523
745
  <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
524
- <svg class="h-4 w-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
746
+ <svg class="h-4 w-4 text-muted" fill="none" stroke="currentColor" viewBox="0 0 24 24">
525
747
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
526
748
  </svg>
527
749
  </div>
528
- <input type="text" id="REPO_SEARCH" placeholder="Rechercher un dépôt..." class="w-full pl-10 pr-4 py-2 bg-slate-900 border border-slate-700 rounded-lg focus:ring-2 focus:ring-blue-500 outline-none text-sm text-slate-200" autocomplete="off" value="${value || ''}">
750
+ <input type="text" id="REPO_SEARCH" placeholder="Rechercher un dépôt..." class="field-input" style="padding-left:2.25rem" autocomplete="off" value="${value || ''}">
529
751
  <div id="REPO_LOADING_INDICATOR" class="absolute inset-y-0 right-0 pr-3 flex items-center hidden">
530
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400"></div>
752
+ <div class="spinner"></div>
531
753
  </div>
532
754
  </div>
533
755
  <div id="REPO_RESULTS" class="search-results hidden"></div>
@@ -538,29 +760,29 @@
538
760
  } else if (field.id === 'BASE_BRANCH') {
539
761
  const currentRepo = config.TARGET_REPO;
540
762
  const branches = selectState.branches[currentRepo] || [];
541
-
763
+
542
764
  if (selectState.loadingBranches[currentRepo]) {
543
765
  return `
544
- <div class="space-y-1">
545
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
546
- <div id="${field.id}_loading" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg text-slate-400 flex items-center">
547
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
766
+ <div>
767
+ <label for="${field.id}" class="field-label">${field.label}</label>
768
+ <div id="${field.id}_loading" class="field-input flex items-center gap-2 text-dim">
769
+ <div class="spinner"></div>
548
770
  Chargement des branches...
549
771
  </div>
550
772
  </div>
551
773
  `;
552
774
  }
553
-
775
+
554
776
  // Recréer les options avec l'option par défaut sélectionnée si value est vide
555
- let branchOptions = `<option value="" ${!value ? 'selected' : ''}>-- Choisir une branche --</option>`;
777
+ let branchOptions = `<option value="" ${!value ? 'selected' : ''}>— Choisir une branche —</option>`;
556
778
  branchOptions += branches.map(branch =>
557
779
  `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
558
780
  ).join('');
559
-
781
+
560
782
  return `
561
- <div class="space-y-1">
562
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
563
- <select id="${field.id}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
783
+ <div>
784
+ <label for="${field.id}" class="field-label">${field.label}</label>
785
+ <select id="${field.id}" class="field-select">
564
786
  ${branchOptions}
565
787
  </select>
566
788
  </div>
@@ -568,44 +790,44 @@
568
790
  } else if (field.id === 'TARGET_BRANCH') {
569
791
  const currentRepo = config.TARGET_REPO;
570
792
  const branches = selectState.branches[currentRepo] || [];
571
-
793
+
572
794
  if (selectState.loadingBranches[currentRepo]) {
573
795
  return `
574
- <div class="space-y-1">
575
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
576
- <div id="${field.id}_loading" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg text-slate-400 flex items-center">
577
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
796
+ <div>
797
+ <label for="${field.id}" class="field-label">${field.label}</label>
798
+ <div id="${field.id}_loading" class="field-input flex items-center gap-2 text-dim">
799
+ <div class="spinner"></div>
578
800
  Chargement des branches...
579
801
  </div>
580
802
  </div>
581
803
  `;
582
804
  }
583
-
584
- let selectOptions = '<option value="">-- Créer une nouvelle branche --</option>';
805
+
806
+ let selectOptions = '<option value="">— Créer une nouvelle branche —</option>';
585
807
  selectOptions += branches.map(branch =>
586
808
  `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
587
809
  ).join('');
588
-
810
+
589
811
  const isCustom = value && !branches.includes(value);
590
812
  const shouldShowCustom = isCustom || value === '';
591
-
813
+
592
814
  return `
593
- <div class="space-y-1">
594
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
815
+ <div>
816
+ <label for="${field.id}" class="field-label">${field.label}</label>
595
817
  <div class="space-y-2">
596
- <select id="${field.id}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
818
+ <select id="${field.id}" class="field-select">
597
819
  ${selectOptions}
598
820
  </select>
599
- <input type="text" id="${field.id}_custom" placeholder="Entrez le nom de la nouvelle branche" value="${isCustom ? value : ''}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100 ${shouldShowCustom ? '' : 'hidden'}">
821
+ <input type="text" id="${field.id}_custom" placeholder="Entrez le nom de la nouvelle branche" value="${isCustom ? value : ''}" class="field-input ${shouldShowCustom ? '' : 'hidden'}">
600
822
  </div>
601
823
  </div>
602
824
  `;
603
825
  }
604
-
826
+
605
827
  return `
606
- <div class="space-y-1">
607
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
608
- <select id="${field.id}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
828
+ <div>
829
+ <label for="${field.id}" class="field-label">${field.label}</label>
830
+ <select id="${field.id}" class="field-select">
609
831
  ${options}
610
832
  </select>
611
833
  </div>
@@ -624,7 +846,7 @@
624
846
  throw new Error(data.error || 'Erreur lors du chargement');
625
847
  }
626
848
  currentConfig = await response.json();
627
-
849
+
628
850
  // Afficher le nom de l'agent si défini
629
851
  const agentBadge = document.getElementById('agentNameBadge');
630
852
  if (currentConfig.AGENT_NAME) {
@@ -633,60 +855,92 @@
633
855
  } else {
634
856
  agentBadge.classList.add('hidden');
635
857
  }
636
-
858
+
637
859
  // Le token est désormais côté serveur, on tente de charger les repos directement
638
860
  await fetchRepositories();
639
861
  if (currentConfig.TARGET_REPO) {
640
862
  await fetchBranches(currentConfig.TARGET_REPO);
641
863
  }
642
-
864
+
643
865
  renderInputs(currentConfig);
644
866
  } catch (error) {
645
867
  showStatus(error.message || 'Erreur lors du chargement de la configuration', 'error');
646
868
  }
647
869
  }
648
870
 
649
- // Render inputs
871
+ // Render a single field based on its type
872
+ function renderField(field, config) {
873
+ if (field.type === 'slider') {
874
+ return `
875
+ <div class="toggle-row">
876
+ <label for="${field.id}" class="text">${field.label}</label>
877
+ <label class="switch">
878
+ <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''}>
879
+ <span class="slider"></span>
880
+ </label>
881
+ </div>
882
+ `;
883
+ }
884
+ if (field.type === 'checkbox') {
885
+ return `
886
+ <div class="toggle-row">
887
+ <label for="${field.id}" class="text">${field.label}</label>
888
+ <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''} class="w-4 h-4">
889
+ </div>
890
+ `;
891
+ }
892
+ if (field.type === 'select') {
893
+ return renderSelectField(field, config[field.id] || '', config);
894
+ }
895
+ const wrapperClass = sshOnlyFields.includes(field.id) ? 'ssh-only' : '';
896
+ return `
897
+ <div class="${wrapperClass}">
898
+ <label for="${field.id}" class="field-label">${field.label}</label>
899
+ <input type="${field.type}" id="${field.id}" value="${config[field.id] || ''}" placeholder="${field.placeholder}" class="field-input">
900
+ </div>
901
+ `;
902
+ }
903
+
904
+ // Render inputs grouped into (collapsible) sections
650
905
  function renderInputs(config) {
651
906
  // CLONE_DIR en lecture seule (variable d'environnement)
652
907
  const cloneDirInfo = config.CLONE_DIR
653
- ? `<div class="p-3 bg-slate-700/50 rounded-lg flex items-center justify-between">
654
- <span class="text-sm font-medium text-slate-400">Répertoire de clonage</span>
655
- <span class="text-sm text-slate-200 font-mono">${config.CLONE_DIR}</span>
908
+ ? `<div class="info-row">
909
+ <span class="text-xs font-medium text-dim">Répertoire de clonage</span>
910
+ <span class="text-xs text-main font-mono">${config.CLONE_DIR}</span>
656
911
  </div>`
657
912
  : '';
658
913
 
659
- inputsContainer.innerHTML = fields.map(field => {
660
- if (field.type === 'slider') {
661
- return `
662
- <div class="flex items-center justify-between p-3 bg-slate-700/50 rounded-lg">
663
- <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
664
- <label class="switch">
665
- <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''}>
666
- <span class="slider"></span>
667
- </label>
668
- </div>
669
- `;
670
- }
671
- if (field.type === 'checkbox') {
914
+ inputsContainer.innerHTML = sections.map(section => {
915
+ const bodyHtml = section.fields
916
+ .map(fid => fieldsById[fid] ? renderField(fieldsById[fid], config) : '')
917
+ .join('');
918
+
919
+ if (!section.collapsible) {
672
920
  return `
673
- <div class="flex items-center space-x-3 p-3 bg-slate-700/50 rounded-lg">
674
- <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''} class="w-5 h-5 text-blue-600 rounded focus:ring-blue-500 bg-slate-900 border-slate-600">
675
- <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
921
+ <div class="form-section">
922
+ <div class="section-static-header">${section.title}</div>
923
+ <div class="section-body space-y-4">${bodyHtml}</div>
676
924
  </div>
677
925
  `;
678
926
  }
679
- if (field.type === 'select') {
680
- return renderSelectField(field, config[field.id] || '', config);
681
- }
927
+
928
+ const open = !!sectionState[section.id];
682
929
  return `
683
- <div class="space-y-1">
684
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
685
- <input type="${field.type}" id="${field.id}" value="${config[field.id] || ''}" placeholder="${field.placeholder}" class="w-full px-4 py-2 bg-slate-900 border border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200 text-slate-100">
930
+ <div class="form-section">
931
+ <button type="button" class="section-header ${open ? 'open' : ''}" data-section="${section.id}">
932
+ <span>${section.title}</span>
933
+ <svg class="chevron w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
934
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"></path>
935
+ </svg>
936
+ </button>
937
+ <div class="section-body space-y-4 ${open ? '' : 'hidden'}" data-section-body="${section.id}">
938
+ ${bodyHtml}
939
+ </div>
686
940
  </div>
687
941
  `;
688
942
  }).join('') + cloneDirInfo;
689
-
943
+
690
944
  if (deleteWorkingDirBtn) {
691
945
  deleteWorkingDirBtn.disabled = !config.REPO_PATH;
692
946
  }
@@ -694,7 +948,7 @@
694
948
  if (cloneBtn) {
695
949
  cloneBtn.disabled = !config.TARGET_REPO || !config.TARGET_BRANCH;
696
950
  }
697
-
951
+
698
952
  setupEventListeners();
699
953
  }
700
954
 
@@ -722,7 +976,28 @@
722
976
  const targetBranchSelect = document.getElementById('TARGET_BRANCH');
723
977
  const repoSearch = document.getElementById('REPO_SEARCH');
724
978
  const resultsContainer = document.getElementById('REPO_RESULTS');
725
-
979
+
980
+ // Sections repliables : bascule sans re-render pour préserver les saisies
981
+ inputsContainer.querySelectorAll('.section-header').forEach(header => {
982
+ header.addEventListener('click', () => {
983
+ const id = header.dataset.section;
984
+ sectionState[id] = !sectionState[id];
985
+ header.classList.toggle('open', sectionState[id]);
986
+ const body = inputsContainer.querySelector(`[data-section-body="${id}"]`);
987
+ if (body) body.classList.toggle('hidden', !sectionState[id]);
988
+ });
989
+ });
990
+
991
+ // Mode de déploiement : masque les champs SSH quand le déploiement local est actif
992
+ const localDeployToggle = document.getElementById('LOCAL_DEPLOY');
993
+ if (localDeployToggle) {
994
+ const applyDeployMode = (isLocal) => {
995
+ inputsContainer.querySelectorAll('.ssh-only').forEach(el => el.classList.toggle('hidden', isLocal));
996
+ };
997
+ applyDeployMode(localDeployToggle.checked);
998
+ localDeployToggle.addEventListener('change', () => applyDeployMode(localDeployToggle.checked));
999
+ }
1000
+
726
1001
  if (repoSearch) {
727
1002
  // Focus: ouvre la liste avec ce qui est déjà là
728
1003
  repoSearch.addEventListener('focus', () => {
@@ -793,7 +1068,7 @@
793
1068
  else config[f.id] = inp.value;
794
1069
  }
795
1070
  });
796
-
1071
+
797
1072
  try {
798
1073
  await fetch(api('api/config'), {
799
1074
  method: 'POST',
@@ -807,12 +1082,12 @@
807
1082
  });
808
1083
  }
809
1084
  });
810
-
1085
+
811
1086
  if (targetBranchSelect) {
812
1087
  targetBranchSelect.addEventListener('change', () => {
813
1088
  const customInput = document.getElementById('TARGET_BRANCH_custom');
814
1089
  if (!customInput) return;
815
-
1090
+
816
1091
  const isNewBranch = targetBranchSelect.value === '';
817
1092
  customInput.classList.toggle('hidden', !isNewBranch);
818
1093
  if (isNewBranch) {
@@ -827,19 +1102,16 @@
827
1102
  function showStatus(message, type) {
828
1103
  const container = document.getElementById('toast-container');
829
1104
  const toast = document.createElement('div');
830
-
831
- const bgColor = type === 'success' ? 'bg-green-900/90' : 'bg-red-900/90';
832
- const borderColor = type === 'success' ? 'border-green-500' : 'border-red-500';
833
- const textColor = type === 'success' ? 'text-green-100' : 'text-red-100';
834
- const icon = type === 'success'
835
- ? '<svg class="w-5 h-5 mr-3 text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>'
836
- : '<svg class="w-5 h-5 mr-3 text-red-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
837
-
838
- toast.className = `toast flex items-center p-4 rounded-lg border shadow-xl ${bgColor} ${borderColor} ${textColor}`;
1105
+
1106
+ const icon = type === 'success'
1107
+ ? '<svg class="w-4 h-4 shrink-0" style="color:var(--accent)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path></svg>'
1108
+ : '<svg class="w-4 h-4 shrink-0" style="color:var(--danger)" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path></svg>';
1109
+
1110
+ toast.className = `toast ${type === 'success' ? 'toast-success' : 'toast-error'}`;
839
1111
  toast.innerHTML = `${icon}<span class="flex-1">${message}</span>`;
840
-
1112
+
841
1113
  container.appendChild(toast);
842
-
1114
+
843
1115
  // Supprimer le toast après l'animation (5s total)
844
1116
  setTimeout(() => {
845
1117
  toast.remove();
@@ -898,7 +1170,7 @@
898
1170
  try {
899
1171
  restartBtn.disabled = true;
900
1172
  restartBtn.innerHTML = '<span>Redémarrage en cours...</span>';
901
-
1173
+
902
1174
  const response = await fetch(api('api/restart'), { method: 'POST' });
903
1175
 
904
1176
  if (response.ok) {
@@ -923,7 +1195,7 @@
923
1195
  try {
924
1196
  stopBtn.disabled = true;
925
1197
  stopBtn.innerHTML = '<span>Arrêt en cours...</span>';
926
-
1198
+
927
1199
  const response = await fetch(api('api/stop'), { method: 'POST' });
928
1200
 
929
1201
  if (response.ok) {
@@ -947,7 +1219,7 @@
947
1219
  try {
948
1220
  deleteWorkingDirBtn.disabled = true;
949
1221
  deleteWorkingDirBtn.innerHTML = '<span>Suppression...</span>';
950
-
1222
+
951
1223
  const response = await fetch(api('api/delete-working-dir'), { method: 'POST' });
952
1224
 
953
1225
  if (response.ok) {
@@ -975,7 +1247,7 @@
975
1247
  try {
976
1248
  cloneBtn.disabled = true;
977
1249
  cloneBtn.innerHTML = '<span>Clonage...</span>';
978
-
1250
+
979
1251
  const response = await fetch(api('api/clone'), { method: 'POST' });
980
1252
 
981
1253
  if (response.ok) {