restricted-github-mcp 1.1.39 → 1.1.41

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
@@ -1,841 +1,992 @@
1
- <!DOCTYPE html>
2
- <html lang="fr" class="dark">
3
- <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>GitHub MCP</title>
7
- <script src="https://cdn.tailwindcss.com"></script>
8
- <style>
9
- body {
10
- background-color: #0f172a;
11
- color: #f8fafc;
12
- }
13
-
14
- /* Layout */
15
- .app-container {
16
- display: grid;
17
- grid-template-columns: 320px 1fr;
18
- gap: 2rem;
19
- max-width: 1200px;
20
- margin: 0 auto;
21
- }
22
-
23
- @media (max-width: 768px) {
24
- .app-container {
25
- grid-template-columns: 1fr;
26
- }
27
- }
28
-
29
- /* iPhone style slider */
30
- .switch {
31
- position: relative;
32
- display: inline-block;
33
- width: 44px;
34
- height: 24px;
35
- flex-shrink: 0;
36
- }
37
-
38
- .switch input {
39
- opacity: 0;
40
- width: 0;
41
- height: 0;
42
- }
43
-
44
- .slider {
45
- position: absolute;
46
- 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;
54
- }
55
-
56
- .slider:before {
57
- position: absolute;
58
- content: "";
59
- height: 18px;
60
- width: 18px;
61
- left: 3px;
62
- bottom: 3px;
63
- background-color: white;
64
- transition: .4s;
65
- border-radius: 50%;
66
- }
67
-
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 */
77
- .search-results {
78
- position: absolute;
79
- top: 100%;
80
- left: 0;
81
- right: 0;
82
- z-index: 50;
83
- background-color: #0f172a;
84
- border: 1px solid #334155;
85
- border-radius: 0.5rem;
86
- margin-top: 0.25rem;
87
- max-height: 15rem;
88
- 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);
90
- }
91
-
92
- .search-result-item {
93
- padding: 0.5rem 1rem;
94
- cursor: pointer;
95
- transition: background-color 0.2s;
96
- font-size: 0.875rem;
97
- }
98
-
99
- .search-result-item:hover {
100
- background-color: #1e293b;
101
- color: #60a5fa;
102
- }
103
-
104
- .search-result-item.active {
105
- background-color: #1e293b;
106
- color: #60a5fa;
107
- outline: 1px solid #3b82f6;
108
- }
109
-
110
- .search-result-item.selected {
111
- background-color: #2563eb;
112
- color: white;
113
- }
114
-
115
- /* Toasts */
116
- .toast {
117
- animation: slideIn 0.3s ease-out forwards, fadeOut 0.3s ease-in forwards 4.7s;
118
- max-width: 24rem;
119
- }
120
-
121
- @keyframes slideIn {
122
- from { transform: translateX(100%); opacity: 0; }
123
- to { transform: translateX(0); opacity: 1; }
124
- }
125
-
126
- @keyframes fadeOut {
127
- from { opacity: 1; }
128
- to { opacity: 0; }
129
- }
130
- </style>
131
- </head>
132
- <body class="min-h-screen p-8">
133
- <div class="app-container">
134
- <!-- 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-6 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="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path>
140
- </svg>
141
- Actions
142
- </h2>
143
-
144
- <div class="space-y-4">
145
- <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">
146
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
147
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
148
- </svg>
149
- <span>Sauvegarder</span>
150
- </button>
151
-
152
- <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">
153
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
154
- <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>
155
- </svg>
156
- <span>Cloner le dépôt</span>
157
- </button>
158
-
159
- <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">
160
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
161
- <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>
162
- </svg>
163
- <span>Redémarrer</span>
164
- </button>
165
-
166
- <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">
167
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
168
- <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>
169
- </svg>
170
- <span>Nettoyer Repo</span>
171
- </button>
172
-
173
- <div class="border-t border-slate-700 pt-4">
174
- <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">
175
- <svg class="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
176
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
177
- </svg>
178
- <span>Arrêter</span>
179
- </button>
180
- </div>
181
- </div>
182
- </div>
183
- </aside>
184
-
185
- <!-- Main Content -->
186
- <main class="bg-slate-800 rounded-xl shadow-2xl p-8 border border-slate-700">
187
- <div class="flex items-center gap-4 mb-6">
188
- <h1 class="text-3xl font-bold text-blue-400 flex items-center">
189
- <svg class="w-8 h-8 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
190
- <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>
191
- </svg>
192
- GitHub MCP
193
- </h1>
194
- <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>
195
- </div>
196
-
197
- <form id="configForm" class="space-y-6">
198
- <div class="space-y-4" id="inputs">
199
- <!-- Inputs will be injected here -->
200
- <div class="flex justify-center p-8">
201
- <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-400"></div>
202
- </div>
203
- </div>
204
- </form>
205
-
206
- <div class="mt-8 text-sm text-slate-400 border-t border-slate-700 pt-4 italic">
207
- Note: Le serveur MCP doit être redémarré pour appliquer les changements.
208
- </div>
209
- </main>
210
- </div>
211
-
212
- <!-- Toast Container -->
213
- <div id="toast-container" class="fixed bottom-8 right-8 z-50 flex flex-col gap-3"></div>
214
-
215
- <script>
216
- // Détection automatique du base path pour fonctionner via proxy
217
- const pathname = window.location.pathname;
218
- const BASE_PATH = pathname.includes('.')
219
- ? pathname.replace(/\/[^/]*$/, '') // /github-mcp/index.html -> /github-mcp
220
- : pathname.endsWith('/')
221
- ? pathname.slice(0, -1) // /github-mcp/ -> /github-mcp
222
- : pathname === '/' ? '' : pathname; // /github-mcp -> /github-mcp, / -> ''
223
-
224
- // Helper pour construire les URLs d'API
225
- const api = (endpoint) => `${BASE_PATH}/${endpoint}`.replace(/\/+/g, '/');
226
-
227
- const form = document.getElementById('configForm');
228
- const inputsContainer = document.getElementById('inputs');
229
- const restartBtn = document.getElementById('restartBtn');
230
- const stopBtn = document.getElementById('stopBtn');
231
- const deleteWorkingDirBtn = document.getElementById('deleteWorkingDirBtn');
232
- const cloneBtn = document.getElementById('cloneBtn');
233
-
234
- const fields = [
235
- { id: 'GITHUB_TOKEN', label: 'GitHub Token', type: 'password', placeholder: 'ghp_...' },
236
- { id: 'TARGET_REPO', label: 'Target Repository', type: 'select', placeholder: 'owner/repo' },
237
- { id: 'BASE_BRANCH', label: 'Base Branch', type: 'select', placeholder: 'main' },
238
- { id: 'TARGET_BRANCH', label: 'Target Branch', type: 'select', placeholder: 'feature-branch' },
239
- { id: 'PROJECT_PATH', label: 'Parent Projects Path', type: 'text', placeholder: 'C:\\Users\\...\\Documents\\code' },
240
- { id: 'READ_ONLY', label: 'Mode Lecture Seule', type: 'slider' },
241
- { id: 'INCLUDE_PUBLIC', label: 'Inclure les dépôts publics', type: 'slider' },
242
- { id: 'INCLUDE_NOT_OWNED', label: 'Inclure les dépôts dont je ne suis pas propriétaire', type: 'slider' }
243
- ];
244
-
245
- // État de l'application
246
- let currentConfig = {};
247
- const selectState = {
248
- repos: [],
249
- branches: {},
250
- loadingRepos: false,
251
- loadingBranches: {}
252
- };
253
-
254
- let searchTimeout;
255
- let activeIndex = -1;
256
-
257
- // Fetch repositories
258
- async function fetchRepositories(token, searchTerm = '') {
259
- if (!token || token.length < 10) return [];
260
-
261
- const loadingIndicator = document.getElementById('REPO_LOADING_INDICATOR');
262
- if (loadingIndicator) loadingIndicator.classList.remove('hidden');
263
-
264
- try {
265
- const url = api(`api/repositories${searchTerm ? `?search=${encodeURIComponent(searchTerm)}` : ''}`);
266
- const response = await fetch(url);
267
- if (response.ok) {
268
- selectState.repos = await response.json();
269
- renderRepoResults();
270
- }
271
- } catch (error) {
272
- console.warn('Error fetching repositories:', error);
273
- } finally {
274
- if (loadingIndicator) loadingIndicator.classList.add('hidden');
275
- }
276
- }
277
-
278
- // Affiche les résultats chargés dans le DOM
279
- function renderRepoResults() {
280
- const resultsContainer = document.getElementById('REPO_RESULTS');
281
- if (!resultsContainer) return;
282
-
283
- if (selectState.repos.length > 0) {
284
- const currentRepo = document.getElementById('TARGET_REPO')?.value;
285
- resultsContainer.innerHTML = selectState.repos.map((repo, index) => `
286
- <div class="search-result-item ${repo === currentRepo ? 'selected' : ''}" data-index="${index}" onclick="selectRepo('${repo}')">
287
- ${repo}
288
- ${repo === currentRepo ? '<span class="float-right text-blue-400">✓</span>' : ''}
289
- </div>
290
- `).join('');
291
- resultsContainer.classList.remove('hidden');
292
- activeIndex = -1;
293
- } else {
294
- resultsContainer.innerHTML = '<div class="p-3 text-slate-500 text-sm">Aucun résultat</div>';
295
- resultsContainer.classList.remove('hidden');
296
- }
297
- }
298
-
299
- // Sélectionner un repo depuis la liste custom
300
- window.selectRepo = async function(repo) {
301
- const searchInput = document.getElementById('REPO_SEARCH');
302
- const hiddenInput = document.getElementById('TARGET_REPO');
303
- const resultsContainer = document.getElementById('REPO_RESULTS');
304
-
305
- if (searchInput) searchInput.value = repo;
306
- if (hiddenInput) {
307
- hiddenInput.value = repo;
308
- currentConfig.TARGET_REPO = repo;
309
- }
310
- if (resultsContainer) resultsContainer.classList.add('hidden');
311
-
312
- // Charger les branches pour ce repo
313
- const token = document.getElementById('GITHUB_TOKEN')?.value;
314
- if (token) {
315
- await fetchBranches(repo, token);
316
- updateSelectUI();
317
- }
318
- };
319
-
320
- // Fetch branches for a repository
321
- async function fetchBranches(repo, token) {
322
- if (!repo || !token) return [];
323
-
324
- selectState.loadingBranches[repo] = true;
325
- updateSelectUI();
326
-
327
- try {
328
- const url = api(`api/branches?repo=${encodeURIComponent(repo)}`);
329
- const response = await fetch(url);
330
- if (response.ok) {
331
- selectState.branches[repo] = await response.json();
332
- }
333
- } catch (error) {
334
- console.error('[fetchBranches] Fetch error:', error);
335
- selectState.branches[repo] = [];
336
- } finally {
337
- selectState.loadingBranches[repo] = false;
338
- updateSelectUI();
339
- }
340
- }
341
-
342
- // Render select field
343
- function renderSelectField(field, value, config) {
344
- let options = '<option value="">-- Sélectionner une option --</option>';
345
-
346
- if (field.id === 'TARGET_REPO') {
347
- if (selectState.loadingRepos && selectState.repos.length === 0) {
348
- return `
349
- <div class="space-y-1">
350
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
351
- <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">
352
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
353
- Chargement des dépôts...
354
- </div>
355
- </div>
356
- `;
357
- }
358
-
359
- options += selectState.repos.map(repo =>
360
- `<option value="${repo}" ${value === repo ? 'selected' : ''}>${repo}</option>`
361
- ).join('');
362
-
363
- if (value && !selectState.repos.includes(value)) {
364
- options += `<option value="${value}" selected>${value}</option>`;
365
- }
366
-
367
- return `
368
- <div class="space-y-1">
369
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
370
- <div class="relative">
371
- <div class="relative">
372
- <div class="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
373
- <svg class="h-4 w-4 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
374
- <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>
375
- </svg>
376
- </div>
377
- <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 || ''}">
378
- <div id="REPO_LOADING_INDICATOR" class="absolute inset-y-0 right-0 pr-3 flex items-center hidden">
379
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400"></div>
380
- </div>
381
- </div>
382
- <div id="REPO_RESULTS" class="search-results hidden"></div>
383
- <input type="hidden" id="${field.id}" value="${value || ''}">
384
- </div>
385
- </div>
386
- `;
387
- } else if (field.id === 'BASE_BRANCH') {
388
- const currentRepo = config.TARGET_REPO;
389
- const branches = selectState.branches[currentRepo] || [];
390
-
391
- if (selectState.loadingBranches[currentRepo]) {
392
- return `
393
- <div class="space-y-1">
394
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
395
- <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">
396
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
397
- Chargement des branches...
398
- </div>
399
- </div>
400
- `;
401
- }
402
-
403
- options += branches.map(branch =>
404
- `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
405
- ).join('');
406
-
407
- // Préserver la valeur actuelle si elle n'est pas dans la liste (pendant le chargement ou si elle est custom)
408
- if (value && !branches.includes(value)) {
409
- options += `<option value="${value}" selected>${value}</option>`;
410
- }
411
- } else if (field.id === 'TARGET_BRANCH') {
412
- const currentRepo = config.TARGET_REPO;
413
- const branches = selectState.branches[currentRepo] || [];
414
-
415
- if (selectState.loadingBranches[currentRepo]) {
416
- return `
417
- <div class="space-y-1">
418
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
419
- <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">
420
- <div class="animate-spin rounded-full h-4 w-4 border-b-2 border-blue-400 mr-2"></div>
421
- Chargement des branches...
422
- </div>
423
- </div>
424
- `;
425
- }
426
-
427
- let selectOptions = '<option value="">-- Créer une nouvelle branche --</option>';
428
- selectOptions += branches.map(branch =>
429
- `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
430
- ).join('');
431
-
432
- const isCustom = value && !branches.includes(value);
433
- const shouldShowCustom = isCustom || value === '';
434
-
435
- return `
436
- <div class="space-y-1">
437
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
438
- <div class="space-y-2">
439
- <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">
440
- ${selectOptions}
441
- </select>
442
- <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'}">
443
- </div>
444
- </div>
445
- `;
446
- }
447
-
448
- return `
449
- <div class="space-y-1">
450
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
451
- <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">
452
- ${options}
453
- </select>
454
- </div>
455
- `;
456
- }
457
-
458
- // Fetch current config
459
- async function fetchConfig() {
460
- try {
461
- const response = await fetch(api('api/config'));
462
- if (!response.ok) {
463
- const data = await response.json().catch(() => ({ error: 'Erreur lors du chargement' }));
464
- throw new Error(data.error || 'Erreur lors du chargement');
465
- }
466
- currentConfig = await response.json();
467
-
468
- // Afficher le nom de l'agent si défini
469
- const agentBadge = document.getElementById('agentNameBadge');
470
- if (currentConfig.AGENT_NAME) {
471
- agentBadge.textContent = currentConfig.AGENT_NAME;
472
- agentBadge.classList.remove('hidden');
473
- } else {
474
- agentBadge.classList.add('hidden');
475
- }
476
-
477
- if (currentConfig.GITHUB_TOKEN) {
478
- await fetchRepositories(currentConfig.GITHUB_TOKEN);
479
- if (currentConfig.TARGET_REPO) {
480
- await fetchBranches(currentConfig.TARGET_REPO, currentConfig.GITHUB_TOKEN);
481
- }
482
- }
483
-
484
- renderInputs(currentConfig);
485
- } catch (error) {
486
- showStatus(error.message || 'Erreur lors du chargement de la configuration', 'error');
487
- }
488
- }
489
-
490
- // Render inputs
491
- function renderInputs(config) {
492
- inputsContainer.innerHTML = fields.map(field => {
493
- // ... (reste de la fonction identique)
494
- if (field.type === 'slider') {
495
- return `
496
- <div class="flex items-center justify-between p-3 bg-slate-700/50 rounded-lg">
497
- <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
498
- <label class="switch">
499
- <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''}>
500
- <span class="slider"></span>
501
- </label>
502
- </div>
503
- `;
504
- }
505
- if (field.type === 'checkbox') {
506
- return `
507
- <div class="flex items-center space-x-3 p-3 bg-slate-700/50 rounded-lg">
508
- <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">
509
- <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
510
- </div>
511
- `;
512
- }
513
- if (field.type === 'select') {
514
- return renderSelectField(field, config[field.id] || '', config);
515
- }
516
- return `
517
- <div class="space-y-1">
518
- <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
519
- <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">
520
- </div>
521
- `;
522
- }).join('');
523
-
524
- if (deleteWorkingDirBtn) {
525
- deleteWorkingDirBtn.disabled = !config.REPO_PATH;
526
- }
527
-
528
- if (cloneBtn) {
529
- // Le clonage est possible si on a un repo, une branche et un chemin de projet
530
- cloneBtn.disabled = !config.TARGET_REPO || !config.TARGET_BRANCH || !config.PROJECT_PATH;
531
- }
532
-
533
- setupEventListeners();
534
- }
535
-
536
- // Update select UI
537
- function updateSelectUI() {
538
- // Mettre à jour currentConfig à partir du DOM pour les éléments qui existent
539
- fields.forEach(field => {
540
- const input = document.getElementById(field.id);
541
- if (input) {
542
- if (field.type === 'checkbox' || field.type === 'slider') {
543
- currentConfig[field.id] = input.checked;
544
- } else if (field.id === 'TARGET_BRANCH') {
545
- const customInput = document.getElementById('TARGET_BRANCH_custom');
546
- currentConfig[field.id] = (input.value === '' && customInput && customInput.value) ? customInput.value : input.value;
547
- } else {
548
- currentConfig[field.id] = input.value;
549
- }
550
- }
551
- });
552
- renderInputs(currentConfig);
553
- }
554
-
555
- // Setup event listeners
556
- function setupEventListeners() {
557
- const tokenInput = document.getElementById('GITHUB_TOKEN');
558
- const targetBranchSelect = document.getElementById('TARGET_BRANCH');
559
- const repoSearch = document.getElementById('REPO_SEARCH');
560
- const resultsContainer = document.getElementById('REPO_RESULTS');
561
-
562
- if (repoSearch) {
563
- // Focus: ouvre la liste avec ce qui est déjà
564
- repoSearch.addEventListener('focus', () => {
565
- if (selectState.repos.length > 0) {
566
- renderRepoResults();
567
- } else {
568
- // Si vraiment vide, on tente de charger une fois
569
- const token = document.getElementById('GITHUB_TOKEN')?.value;
570
- if (token) fetchRepositories(token);
571
- }
572
- });
573
-
574
- // Clavier: navigation avec flèches et Enter
575
- repoSearch.addEventListener('keydown', (e) => {
576
- const items = resultsContainer.querySelectorAll('.search-result-item');
577
- if (items.length === 0) return;
578
-
579
- if (e.key === 'ArrowDown') {
580
- e.preventDefault();
581
- activeIndex = Math.min(activeIndex + 1, items.length - 1);
582
- updateActiveItem(items);
583
- } else if (e.key === 'ArrowUp') {
584
- e.preventDefault();
585
- activeIndex = Math.max(activeIndex - 1, 0);
586
- updateActiveItem(items);
587
- } else if (e.key === 'Enter') {
588
- e.preventDefault();
589
- if (activeIndex >= 0) {
590
- selectRepo(selectState.repos[activeIndex]);
591
- }
592
- } else if (e.key === 'Escape') {
593
- resultsContainer.classList.add('hidden');
594
- }
595
- });
596
-
597
- repoSearch.addEventListener('input', (e) => {
598
- const term = e.target.value;
599
- clearTimeout(searchTimeout);
600
- searchTimeout = setTimeout(() => {
601
- const token = document.getElementById('GITHUB_TOKEN')?.value;
602
- fetchRepositories(token, term);
603
- }, 500);
604
- });
605
- }
606
-
607
- function updateActiveItem(items) {
608
- items.forEach((item, index) => {
609
- item.classList.toggle('active', index === activeIndex);
610
- if (index === activeIndex) {
611
- item.scrollIntoView({ block: 'nearest' });
612
- }
613
- });
614
- }
615
-
616
- document.addEventListener('click', (e) => {
617
- if (resultsContainer && !resultsContainer.contains(e.target) && e.target !== repoSearch) {
618
- resultsContainer.classList.add('hidden');
619
- }
620
- });
621
-
622
- if (tokenInput) {
623
- tokenInput.addEventListener('change', async () => {
624
- const token = tokenInput.value;
625
- if (token && token.length > 10) {
626
- selectState.branches = {};
627
- await fetchRepositories(token);
628
- }
629
- });
630
- }
631
-
632
- ['INCLUDE_PUBLIC', 'INCLUDE_NOT_OWNED'].forEach(id => {
633
- const input = document.getElementById(id);
634
- if (input) {
635
- input.addEventListener('change', async () => {
636
- const token = document.getElementById('GITHUB_TOKEN')?.value;
637
- if (token && token.length > 10) {
638
- const config = {};
639
- fields.forEach(f => {
640
- const inp = document.getElementById(f.id);
641
- if (inp) {
642
- if (f.type === 'checkbox' || f.type === 'slider') config[f.id] = inp.checked;
643
- else config[f.id] = inp.value;
644
- }
645
- });
646
-
647
- try {
648
- await fetch(api('api/config'), {
649
- method: 'POST',
650
- headers: { 'Content-Type': 'application/json' },
651
- body: JSON.stringify(config)
652
- });
653
- await fetchRepositories(token, repoSearch?.value || '');
654
- } catch (e) {
655
- console.error('Erreur lors de la mise à jour auto:', e);
656
- }
657
- }
658
- });
659
- }
660
- });
661
-
662
- if (targetBranchSelect) {
663
- targetBranchSelect.addEventListener('change', () => {
664
- const customInput = document.getElementById('TARGET_BRANCH_custom');
665
- if (!customInput) return;
666
-
667
- const isNewBranch = targetBranchSelect.value === '';
668
- customInput.classList.toggle('hidden', !isNewBranch);
669
- if (isNewBranch) {
670
- customInput.focus();
671
- } else {
672
- customInput.value = '';
673
- }
674
- });
675
- }
676
- }
677
-
678
- function showStatus(message, type) {
679
- const container = document.getElementById('toast-container');
680
- const toast = document.createElement('div');
681
-
682
- const bgColor = type === 'success' ? 'bg-green-900/90' : 'bg-red-900/90';
683
- const borderColor = type === 'success' ? 'border-green-500' : 'border-red-500';
684
- const textColor = type === 'success' ? 'text-green-100' : 'text-red-100';
685
- const icon = type === 'success'
686
- ? '<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>'
687
- : '<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>';
688
-
689
- toast.className = `toast flex items-center p-4 rounded-lg border shadow-xl ${bgColor} ${borderColor} ${textColor}`;
690
- toast.innerHTML = `${icon}<span class="flex-1">${message}</span>`;
691
-
692
- container.appendChild(toast);
693
-
694
- // Supprimer le toast après l'animation (5s total)
695
- setTimeout(() => {
696
- toast.remove();
697
- }, 5000);
698
- }
699
-
700
- // Uniformisation: addEventListener au lieu de onsubmit/onclick
701
- form.addEventListener('submit', async (e) => {
702
- e.preventDefault();
703
- const config = {};
704
- fields.forEach(field => {
705
- const input = document.getElementById(field.id);
706
- if (!input) return; // Protection contre les éléments manquants
707
- if (field.type === 'checkbox' || field.type === 'slider') {
708
- config[field.id] = input.checked;
709
- } else if (field.id === 'TARGET_BRANCH') {
710
- const customInput = document.getElementById(`${field.id}_custom`);
711
- config[field.id] = customInput && customInput.value ? customInput.value : input.value;
712
- } else {
713
- config[field.id] = input.value;
714
- }
715
- });
716
-
717
- try {
718
- const response = await fetch(api('api/config'), {
719
- method: 'POST',
720
- headers: { 'Content-Type': 'application/json' },
721
- body: JSON.stringify(config)
722
- });
723
-
724
- if (response.ok) {
725
- showStatus('Configuration enregistrée avec succès !', 'success');
726
- } else {
727
- const data = await response.json().catch(() => ({ error: 'Erreur inconnue' }));
728
- throw new Error(data.error || 'Erreur lors de l\'enregistrement');
729
- }
730
- } catch (error) {
731
- showStatus(error.message, 'error');
732
- }
733
- });
734
-
735
- restartBtn.addEventListener('click', async () => {
736
- if (!confirm('Êtes-vous sûr de vouloir redémarrer le serveur MCP ? Cela coupera la connexion actuelle pour appliquer les changements.')) {
737
- return;
738
- }
739
-
740
- const originalHTML = restartBtn.innerHTML; // Sauvegarder le HTML original
741
- try {
742
- restartBtn.disabled = true;
743
- restartBtn.innerHTML = '<span>Redémarrage en cours...</span>';
744
-
745
- const response = await fetch(api('api/restart'), { method: 'POST' });
746
-
747
- if (response.ok) {
748
- showStatus('Signal de redémarrage envoyé. Le serveur va se relancer...', 'success');
749
- setTimeout(() => window.location.reload(), 3000);
750
- } else {
751
- throw new Error('Erreur lors de la demande de redémarrage');
752
- }
753
- } catch (error) {
754
- showStatus(error.message, 'error');
755
- restartBtn.disabled = false;
756
- restartBtn.innerHTML = originalHTML; // Restaurer le HTML original
757
- }
758
- });
759
-
760
- stopBtn.addEventListener('click', async () => {
761
- if (!confirm('Êtes-vous sûr de vouloir arrêter COMPLÈTEMENT le serveur MCP ?')) {
762
- return;
763
- }
764
-
765
- const originalHTML = stopBtn.innerHTML; // Sauvegarder le HTML original
766
- try {
767
- stopBtn.disabled = true;
768
- stopBtn.innerHTML = '<span>Arrêt en cours...</span>';
769
-
770
- const response = await fetch(api('api/stop'), { method: 'POST' });
771
-
772
- if (response.ok) {
773
- showStatus('Le serveur s\'arrête. Cette page ne sera plus disponible.', 'success');
774
- } else {
775
- throw new Error('Erreur lors de la demande d\'arrêt');
776
- }
777
- } catch (error) {
778
- showStatus(error.message, 'error');
779
- stopBtn.disabled = false;
780
- stopBtn.innerHTML = originalHTML; // Restaurer le HTML original
781
- }
782
- });
783
-
784
- deleteWorkingDirBtn.addEventListener('click', async () => {
785
- if (!confirm('Êtes-vous sûr de vouloir SUPPRIMER physiquement le dossier de travail local ? Cette action est irréversible et vous devrez effectuer un nouveau "clone" pour utiliser les outils Git.')) {
786
- return;
787
- }
788
-
789
- const originalHTML = deleteWorkingDirBtn.innerHTML;
790
- try {
791
- deleteWorkingDirBtn.disabled = true;
792
- deleteWorkingDirBtn.innerHTML = '<span>Suppression...</span>';
793
-
794
- const response = await fetch(api('api/delete-working-dir'), { method: 'POST' });
795
-
796
- if (response.ok) {
797
- showStatus('Dossier de travail supprimé avec succès.', 'success');
798
- currentConfig.REPO_PATH = '';
799
- deleteWorkingDirBtn.disabled = true;
800
- } else {
801
- const data = await response.json().catch(() => ({ error: 'Erreur lors de la suppression' }));
802
- throw new Error(data.error || 'Erreur lors de la suppression');
803
- }
804
- } catch (error) {
805
- showStatus(error.message, 'error');
806
- } finally {
807
- deleteWorkingDirBtn.innerHTML = originalHTML;
808
- deleteWorkingDirBtn.disabled = !currentConfig.REPO_PATH;
809
- }
810
- });
811
-
812
- cloneBtn.addEventListener('click', async () => {
813
- if (!confirm('Voulez-vous lancer le clonage du dépôt ? Assurez-vous d\'avoir enregistré la configuration (Parent Projects Path) avant.')) {
814
- return;
815
- }
816
-
817
- const originalHTML = cloneBtn.innerHTML;
818
- try {
819
- cloneBtn.disabled = true;
820
- cloneBtn.innerHTML = '<span>Clonage...</span>';
821
-
822
- const response = await fetch(api('api/clone'), { method: 'POST' });
823
-
824
- if (response.ok) {
825
- showStatus('Le processus de clonage a été lancé en arrière-plan. Vérifiez les logs pour le résultat.', 'success');
826
- } else {
827
- const data = await response.json().catch(() => ({ error: 'Erreur lors du clonage' }));
828
- throw new Error(data.error || 'Erreur lors du clonage');
829
- }
830
- } catch (error) {
831
- showStatus(error.message, 'error');
832
- cloneBtn.disabled = false;
833
- } finally {
834
- cloneBtn.innerHTML = originalHTML;
835
- }
836
- });
837
-
838
- fetchConfig();
839
- </script>
840
- </body>
841
- </html>
1
+ <!DOCTYPE html>
2
+ <html lang="fr" class="dark">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>GitHub MCP</title>
7
+ <script src="https://cdn.tailwindcss.com"></script>
8
+ <style>
9
+ body {
10
+ background-color: #0f172a;
11
+ color: #f8fafc;
12
+ }
13
+
14
+ /* Layout */
15
+ .app-container {
16
+ display: grid;
17
+ grid-template-columns: 320px 1fr;
18
+ gap: 2rem;
19
+ max-width: 1200px;
20
+ margin: 0 auto;
21
+ }
22
+
23
+ @media (max-width: 768px) {
24
+ .app-container {
25
+ grid-template-columns: 1fr;
26
+ }
27
+ }
28
+
29
+ /* iPhone style slider */
30
+ .switch {
31
+ position: relative;
32
+ display: inline-block;
33
+ width: 44px;
34
+ height: 24px;
35
+ flex-shrink: 0;
36
+ }
37
+
38
+ .switch input {
39
+ opacity: 0;
40
+ width: 0;
41
+ height: 0;
42
+ }
43
+
44
+ .slider {
45
+ position: absolute;
46
+ 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;
54
+ }
55
+
56
+ .slider:before {
57
+ position: absolute;
58
+ content: "";
59
+ height: 18px;
60
+ width: 18px;
61
+ left: 3px;
62
+ bottom: 3px;
63
+ background-color: white;
64
+ transition: .4s;
65
+ border-radius: 50%;
66
+ }
67
+
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 */
77
+ .search-results {
78
+ position: absolute;
79
+ top: 100%;
80
+ left: 0;
81
+ right: 0;
82
+ z-index: 50;
83
+ background-color: #0f172a;
84
+ border: 1px solid #334155;
85
+ border-radius: 0.5rem;
86
+ margin-top: 0.25rem;
87
+ max-height: 15rem;
88
+ 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);
90
+ }
91
+
92
+ .search-result-item {
93
+ padding: 0.5rem 1rem;
94
+ cursor: pointer;
95
+ transition: background-color 0.2s;
96
+ font-size: 0.875rem;
97
+ }
98
+
99
+ .search-result-item:hover {
100
+ background-color: #1e293b;
101
+ color: #60a5fa;
102
+ }
103
+
104
+ .search-result-item.active {
105
+ background-color: #1e293b;
106
+ color: #60a5fa;
107
+ outline: 1px solid #3b82f6;
108
+ }
109
+
110
+ .search-result-item.selected {
111
+ background-color: #2563eb;
112
+ color: white;
113
+ }
114
+
115
+ /* Toasts */
116
+ .toast {
117
+ animation: slideIn 0.3s ease-out forwards, fadeOut 0.3s ease-in forwards 4.7s;
118
+ max-width: 24rem;
119
+ }
120
+
121
+ @keyframes slideIn {
122
+ from { transform: translateX(100%); opacity: 0; }
123
+ to { transform: translateX(0); opacity: 1; }
124
+ }
125
+
126
+ @keyframes fadeOut {
127
+ from { opacity: 1; }
128
+ to { opacity: 0; }
129
+ }
130
+ </style>
131
+ </head>
132
+ <body class="min-h-screen p-8">
133
+ <div class="app-container">
134
+ <!-- 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>
143
+
144
+ <!-- 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>
171
+ </svg>
172
+ <span>Sauvegarder</span>
173
+ </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>
178
+ </svg>
179
+ <span>Cloner le dépôt</span>
180
+ </button>
181
+
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>
188
+
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>
195
+
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>
202
+ </button>
203
+ </div>
204
+ </div>
205
+ </div>
206
+ </aside>
207
+
208
+ <!-- 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>
214
+ </svg>
215
+ GitHub MCP
216
+ </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>
218
+ </div>
219
+
220
+ <form id="configForm" class="space-y-6">
221
+ <div class="space-y-4" id="inputs">
222
+ <!-- Inputs will be injected here -->
223
+ <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>
225
+ </div>
226
+ </div>
227
+ </form>
228
+
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.
231
+ </div>
232
+ </main>
233
+ </div>
234
+
235
+ <!-- Toast Container -->
236
+ <div id="toast-container" class="fixed bottom-8 right-8 z-50 flex flex-col gap-3"></div>
237
+
238
+ <script>
239
+ // Détection automatique du base path pour fonctionner via proxy
240
+ const pathname = window.location.pathname;
241
+ const BASE_PATH = pathname.includes('.')
242
+ ? pathname.replace(/\/[^/]*$/, '') // /github-mcp/index.html -> /github-mcp
243
+ : pathname.endsWith('/')
244
+ ? pathname.slice(0, -1) // /github-mcp/ -> /github-mcp
245
+ : pathname === '/' ? '' : pathname; // /github-mcp -> /github-mcp, / -> ''
246
+
247
+ // Helper pour construire les URLs d'API
248
+ const api = (endpoint) => `${BASE_PATH}/${endpoint}`.replace(/\/+/g, '/');
249
+
250
+ const form = document.getElementById('configForm');
251
+ const inputsContainer = document.getElementById('inputs');
252
+ const restartBtn = document.getElementById('restartBtn');
253
+ const stopBtn = document.getElementById('stopBtn');
254
+ const deleteWorkingDirBtn = document.getElementById('deleteWorkingDirBtn');
255
+ const cloneBtn = document.getElementById('cloneBtn');
256
+
257
+ const fields = [
258
+ { id: 'TARGET_REPO', label: 'Target Repository', type: 'select', placeholder: 'owner/repo' },
259
+ { id: 'BASE_BRANCH', label: 'Base Branch', type: 'select', placeholder: 'main' },
260
+ { id: 'TARGET_BRANCH', label: 'Target Branch', type: 'select', placeholder: 'feature-branch' },
261
+ { id: 'PROJECT_PATH', label: 'Parent Projects Path', type: 'text', placeholder: 'C:\\Users\\...\\Documents\\code' },
262
+ { id: 'READ_ONLY', label: 'Mode Lecture Seule', type: 'slider' },
263
+ { id: 'INCLUDE_PUBLIC', label: 'Inclure les dépôts publics', type: 'slider' },
264
+ { id: 'INCLUDE_NOT_OWNED', label: 'Inclure les dépôts dont je ne suis pas propriétaire', type: 'slider' }
265
+ ];
266
+
267
+ // État de l'application
268
+ let currentConfig = {};
269
+ let currentConfigName = '';
270
+ let configNames = [];
271
+ const selectState = {
272
+ repos: [],
273
+ branches: {},
274
+ loadingRepos: false,
275
+ loadingBranches: {}
276
+ };
277
+
278
+ let searchTimeout;
279
+ let activeIndex = -1;
280
+
281
+ // Configuration management
282
+ const configSelector = document.getElementById('configSelector');
283
+ const newConfigBtn = document.getElementById('newConfigBtn');
284
+ const deleteConfigBtn = document.getElementById('deleteConfigBtn');
285
+
286
+ async function fetchConfigs() {
287
+ try {
288
+ const response = await fetch(api('api/configs'));
289
+ if (response.ok) {
290
+ const data = await response.json();
291
+ configNames = data.names || [];
292
+ currentConfigName = data.current || '';
293
+ renderConfigSelector();
294
+ }
295
+ } catch (error) {
296
+ console.warn('Error fetching configs:', error);
297
+ }
298
+ }
299
+
300
+ function renderConfigSelector() {
301
+ configSelector.innerHTML = '<option value="">-- Sélectionner --</option>' +
302
+ configNames.map(name =>
303
+ `<option value="${name}" ${name === currentConfigName ? 'selected' : ''}>${name}</option>`
304
+ ).join('');
305
+
306
+ deleteConfigBtn.disabled = !currentConfigName || configNames.length <= 1;
307
+ }
308
+
309
+ async function activateConfig(name) {
310
+ if (!name) return;
311
+ try {
312
+ const response = await fetch(api(`api/configs/${encodeURIComponent(name)}/activate`), {
313
+ method: 'POST'
314
+ });
315
+ if (response.ok) {
316
+ currentConfigName = name;
317
+ // Reset branches state to force reload for new config
318
+ selectState.branches = {};
319
+ showStatus(`Configuration "${name}" activée`, 'success');
320
+ await fetchConfig();
321
+ } else {
322
+ throw new Error('Erreur lors de l\'activation');
323
+ }
324
+ } catch (error) {
325
+ showStatus(error.message, 'error');
326
+ }
327
+ }
328
+
329
+ async function createNewConfig() {
330
+ const name = prompt('Nom de la nouvelle configuration:');
331
+ if (!name || !name.trim()) return;
332
+
333
+ const trimmedName = name.trim();
334
+ if (configNames.includes(trimmedName)) {
335
+ showStatus('Une configuration avec ce nom existe déjà', 'error');
336
+ return;
337
+ }
338
+
339
+ const newConfig = {
340
+ TARGET_REPO: '',
341
+ TARGET_BRANCH: '',
342
+ BASE_BRANCH: '',
343
+ PROJECT_PATH: currentConfig.PROJECT_PATH || '',
344
+ READ_ONLY: currentConfig.READ_ONLY || false,
345
+ INCLUDE_PUBLIC: currentConfig.INCLUDE_PUBLIC || false,
346
+ INCLUDE_NOT_OWNED: currentConfig.INCLUDE_NOT_OWNED || false
347
+ };
348
+
349
+ try {
350
+ const response = await fetch(api('api/configs'), {
351
+ method: 'POST',
352
+ headers: { 'Content-Type': 'application/json' },
353
+ body: JSON.stringify({ name: trimmedName, config: newConfig })
354
+ });
355
+
356
+ if (response.ok) {
357
+ showStatus(`Configuration "${trimmedName}" créée (basée sur la configuration actuelle)`, 'success');
358
+ await fetchConfigs();
359
+ await activateConfig(trimmedName);
360
+ } else {
361
+ throw new Error('Erreur lors de la création');
362
+ }
363
+ } catch (error) {
364
+ showStatus(error.message, 'error');
365
+ }
366
+ }
367
+
368
+ async function deleteCurrentConfig() {
369
+ if (!currentConfigName) return;
370
+ if (configNames.length <= 1) {
371
+ showStatus('Impossible de supprimer la dernière configuration', 'error');
372
+ return;
373
+ }
374
+
375
+ if (!confirm(`Êtes-vous sûr de vouloir supprimer la configuration "${currentConfigName}" ?`)) {
376
+ return;
377
+ }
378
+
379
+ try {
380
+ const response = await fetch(api(`api/configs/${encodeURIComponent(currentConfigName)}`), {
381
+ method: 'DELETE'
382
+ });
383
+
384
+ if (response.ok) {
385
+ showStatus(`Configuration "${currentConfigName}" supprimée`, 'success');
386
+ await fetchConfigs();
387
+ // Activate first remaining config
388
+ if (configNames.length > 0) {
389
+ await activateConfig(configNames[0]);
390
+ }
391
+ } else {
392
+ throw new Error('Erreur lors de la suppression');
393
+ }
394
+ } catch (error) {
395
+ showStatus(error.message, 'error');
396
+ }
397
+ }
398
+
399
+ configSelector.addEventListener('change', (e) => {
400
+ if (e.target.value) {
401
+ activateConfig(e.target.value);
402
+ }
403
+ });
404
+
405
+ newConfigBtn.addEventListener('click', createNewConfig);
406
+ deleteConfigBtn.addEventListener('click', deleteCurrentConfig);
407
+
408
+ // Fetch repositories
409
+ async function fetchRepositories(searchTerm = '') {
410
+ const loadingIndicator = document.getElementById('REPO_LOADING_INDICATOR');
411
+ if (loadingIndicator) loadingIndicator.classList.remove('hidden');
412
+
413
+ try {
414
+ const url = api(`api/repositories${searchTerm ? `?search=${encodeURIComponent(searchTerm)}` : ''}`);
415
+ const response = await fetch(url);
416
+ if (response.ok) {
417
+ selectState.repos = await response.json();
418
+ renderRepoResults();
419
+ }
420
+ } catch (error) {
421
+ console.warn('Error fetching repositories:', error);
422
+ } finally {
423
+ if (loadingIndicator) loadingIndicator.classList.add('hidden');
424
+ }
425
+ }
426
+
427
+ // Affiche les résultats chargés dans le DOM
428
+ function renderRepoResults() {
429
+ const resultsContainer = document.getElementById('REPO_RESULTS');
430
+ if (!resultsContainer) return;
431
+
432
+ if (selectState.repos.length > 0) {
433
+ const currentRepo = document.getElementById('TARGET_REPO')?.value;
434
+ resultsContainer.innerHTML = selectState.repos.map((repo, index) => `
435
+ <div class="search-result-item ${repo === currentRepo ? 'selected' : ''}" data-index="${index}" onclick="selectRepo('${repo}')">
436
+ ${repo}
437
+ ${repo === currentRepo ? '<span class="float-right text-blue-400">✓</span>' : ''}
438
+ </div>
439
+ `).join('');
440
+ resultsContainer.classList.remove('hidden');
441
+ activeIndex = -1;
442
+ } else {
443
+ resultsContainer.innerHTML = '<div class="p-3 text-slate-500 text-sm">Aucun résultat</div>';
444
+ resultsContainer.classList.remove('hidden');
445
+ }
446
+ }
447
+
448
+ // Sélectionner un repo depuis la liste custom
449
+ window.selectRepo = async function(repo) {
450
+ const searchInput = document.getElementById('REPO_SEARCH');
451
+ const hiddenInput = document.getElementById('TARGET_REPO');
452
+ const resultsContainer = document.getElementById('REPO_RESULTS');
453
+
454
+ if (searchInput) searchInput.value = repo;
455
+ if (hiddenInput) {
456
+ hiddenInput.value = repo;
457
+ currentConfig.TARGET_REPO = repo;
458
+ }
459
+ if (resultsContainer) resultsContainer.classList.add('hidden');
460
+
461
+ // Réinitialiser les branches lors du changement de repository
462
+ currentConfig.BASE_BRANCH = '';
463
+ currentConfig.TARGET_BRANCH = '';
464
+
465
+ // Charger les branches pour ce repo (renderInputs is called inside fetchBranches)
466
+ await fetchBranches(repo);
467
+ };
468
+
469
+ // Fetch branches for a repository
470
+ async function fetchBranches(repo, preserveConfig = false) {
471
+ if (!repo) return [];
472
+
473
+ selectState.loadingBranches[repo] = true;
474
+ // Use renderInputs directly to avoid overwriting currentConfig from DOM
475
+ renderInputs(currentConfig);
476
+
477
+ try {
478
+ const url = api(`api/branches?repo=${encodeURIComponent(repo)}`);
479
+ const response = await fetch(url);
480
+ if (response.ok) {
481
+ selectState.branches[repo] = await response.json();
482
+ }
483
+ } catch (error) {
484
+ console.error('[fetchBranches] Fetch error:', error);
485
+ selectState.branches[repo] = [];
486
+ } finally {
487
+ selectState.loadingBranches[repo] = false;
488
+ // Use renderInputs directly to avoid overwriting currentConfig from DOM
489
+ renderInputs(currentConfig);
490
+ }
491
+ }
492
+
493
+ // Render select field
494
+ function renderSelectField(field, value, config) {
495
+ let options = '<option value="">-- Sélectionner une option --</option>';
496
+
497
+ if (field.id === 'TARGET_REPO') {
498
+ if (selectState.loadingRepos && selectState.repos.length === 0) {
499
+ 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>
504
+ Chargement des dépôts...
505
+ </div>
506
+ </div>
507
+ `;
508
+ }
509
+
510
+ options += selectState.repos.map(repo =>
511
+ `<option value="${repo}" ${value === repo ? 'selected' : ''}>${repo}</option>`
512
+ ).join('');
513
+
514
+ if (value && !selectState.repos.includes(value)) {
515
+ options += `<option value="${value}" selected>${value}</option>`;
516
+ }
517
+
518
+ return `
519
+ <div class="space-y-1">
520
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
521
+ <div class="relative">
522
+ <div class="relative">
523
+ <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">
525
+ <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
+ </svg>
527
+ </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 || ''}">
529
+ <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>
531
+ </div>
532
+ </div>
533
+ <div id="REPO_RESULTS" class="search-results hidden"></div>
534
+ <input type="hidden" id="${field.id}" value="${value || ''}">
535
+ </div>
536
+ </div>
537
+ `;
538
+ } else if (field.id === 'BASE_BRANCH') {
539
+ const currentRepo = config.TARGET_REPO;
540
+ const branches = selectState.branches[currentRepo] || [];
541
+
542
+ if (selectState.loadingBranches[currentRepo]) {
543
+ 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>
548
+ Chargement des branches...
549
+ </div>
550
+ </div>
551
+ `;
552
+ }
553
+
554
+ // 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>`;
556
+ branchOptions += branches.map(branch =>
557
+ `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
558
+ ).join('');
559
+
560
+ 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">
564
+ ${branchOptions}
565
+ </select>
566
+ </div>
567
+ `;
568
+ } else if (field.id === 'TARGET_BRANCH') {
569
+ const currentRepo = config.TARGET_REPO;
570
+ const branches = selectState.branches[currentRepo] || [];
571
+
572
+ if (selectState.loadingBranches[currentRepo]) {
573
+ 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>
578
+ Chargement des branches...
579
+ </div>
580
+ </div>
581
+ `;
582
+ }
583
+
584
+ let selectOptions = '<option value="">-- Créer une nouvelle branche --</option>';
585
+ selectOptions += branches.map(branch =>
586
+ `<option value="${branch}" ${value === branch ? 'selected' : ''}>${branch}</option>`
587
+ ).join('');
588
+
589
+ const isCustom = value && !branches.includes(value);
590
+ const shouldShowCustom = isCustom || value === '';
591
+
592
+ return `
593
+ <div class="space-y-1">
594
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
595
+ <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">
597
+ ${selectOptions}
598
+ </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'}">
600
+ </div>
601
+ </div>
602
+ `;
603
+ }
604
+
605
+ 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">
609
+ ${options}
610
+ </select>
611
+ </div>
612
+ `;
613
+ }
614
+
615
+ // Fetch current config
616
+ async function fetchConfig() {
617
+ try {
618
+ // Fetch configs list first
619
+ await fetchConfigs();
620
+
621
+ const response = await fetch(api('api/config'));
622
+ if (!response.ok) {
623
+ const data = await response.json().catch(() => ({ error: 'Erreur lors du chargement' }));
624
+ throw new Error(data.error || 'Erreur lors du chargement');
625
+ }
626
+ currentConfig = await response.json();
627
+
628
+ // Afficher le nom de l'agent si défini
629
+ const agentBadge = document.getElementById('agentNameBadge');
630
+ if (currentConfig.AGENT_NAME) {
631
+ agentBadge.textContent = currentConfig.AGENT_NAME;
632
+ agentBadge.classList.remove('hidden');
633
+ } else {
634
+ agentBadge.classList.add('hidden');
635
+ }
636
+
637
+ // Le token est désormais côté serveur, on tente de charger les repos directement
638
+ await fetchRepositories();
639
+ if (currentConfig.TARGET_REPO) {
640
+ await fetchBranches(currentConfig.TARGET_REPO);
641
+ }
642
+
643
+ renderInputs(currentConfig);
644
+ } catch (error) {
645
+ showStatus(error.message || 'Erreur lors du chargement de la configuration', 'error');
646
+ }
647
+ }
648
+
649
+ // Render inputs
650
+ function renderInputs(config) {
651
+ inputsContainer.innerHTML = fields.map(field => {
652
+ // ... (reste de la fonction identique)
653
+ if (field.type === 'slider') {
654
+ return `
655
+ <div class="flex items-center justify-between p-3 bg-slate-700/50 rounded-lg">
656
+ <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
657
+ <label class="switch">
658
+ <input type="checkbox" id="${field.id}" ${config[field.id] === 'true' || config[field.id] === true ? 'checked' : ''}>
659
+ <span class="slider"></span>
660
+ </label>
661
+ </div>
662
+ `;
663
+ }
664
+ if (field.type === 'checkbox') {
665
+ return `
666
+ <div class="flex items-center space-x-3 p-3 bg-slate-700/50 rounded-lg">
667
+ <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">
668
+ <label for="${field.id}" class="text-sm font-medium text-slate-200">${field.label}</label>
669
+ </div>
670
+ `;
671
+ }
672
+ if (field.type === 'select') {
673
+ return renderSelectField(field, config[field.id] || '', config);
674
+ }
675
+ return `
676
+ <div class="space-y-1">
677
+ <label for="${field.id}" class="block text-sm font-medium text-slate-300">${field.label}</label>
678
+ <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">
679
+ </div>
680
+ `;
681
+ }).join('');
682
+
683
+ if (deleteWorkingDirBtn) {
684
+ deleteWorkingDirBtn.disabled = !config.REPO_PATH;
685
+ }
686
+
687
+ if (cloneBtn) {
688
+ // Le clonage est possible si on a un repo, une branche et un chemin de projet
689
+ cloneBtn.disabled = !config.TARGET_REPO || !config.TARGET_BRANCH || !config.PROJECT_PATH;
690
+ }
691
+
692
+ setupEventListeners();
693
+ }
694
+
695
+ // Update select UI
696
+ function updateSelectUI() {
697
+ // Mettre à jour currentConfig à partir du DOM pour les éléments qui existent
698
+ fields.forEach(field => {
699
+ const input = document.getElementById(field.id);
700
+ if (input) {
701
+ if (field.type === 'checkbox' || field.type === 'slider') {
702
+ currentConfig[field.id] = input.checked;
703
+ } else if (field.id === 'TARGET_BRANCH') {
704
+ const customInput = document.getElementById('TARGET_BRANCH_custom');
705
+ currentConfig[field.id] = (input.value === '' && customInput && customInput.value) ? customInput.value : input.value;
706
+ } else {
707
+ currentConfig[field.id] = input.value;
708
+ }
709
+ }
710
+ });
711
+ renderInputs(currentConfig);
712
+ }
713
+
714
+ // Setup event listeners
715
+ function setupEventListeners() {
716
+ const targetBranchSelect = document.getElementById('TARGET_BRANCH');
717
+ const repoSearch = document.getElementById('REPO_SEARCH');
718
+ const resultsContainer = document.getElementById('REPO_RESULTS');
719
+
720
+ if (repoSearch) {
721
+ // Focus: ouvre la liste avec ce qui est déjà là
722
+ repoSearch.addEventListener('focus', () => {
723
+ if (selectState.repos.length > 0) {
724
+ renderRepoResults();
725
+ } else {
726
+ // Si vraiment vide, on tente de charger une fois
727
+ fetchRepositories();
728
+ }
729
+ });
730
+
731
+ // Clavier: navigation avec flèches et Enter
732
+ repoSearch.addEventListener('keydown', (e) => {
733
+ const items = resultsContainer.querySelectorAll('.search-result-item');
734
+ if (items.length === 0) return;
735
+
736
+ if (e.key === 'ArrowDown') {
737
+ e.preventDefault();
738
+ activeIndex = Math.min(activeIndex + 1, items.length - 1);
739
+ updateActiveItem(items);
740
+ } else if (e.key === 'ArrowUp') {
741
+ e.preventDefault();
742
+ activeIndex = Math.max(activeIndex - 1, 0);
743
+ updateActiveItem(items);
744
+ } else if (e.key === 'Enter') {
745
+ e.preventDefault();
746
+ if (activeIndex >= 0) {
747
+ selectRepo(selectState.repos[activeIndex]);
748
+ }
749
+ } else if (e.key === 'Escape') {
750
+ resultsContainer.classList.add('hidden');
751
+ }
752
+ });
753
+
754
+ repoSearch.addEventListener('input', (e) => {
755
+ const term = e.target.value;
756
+ clearTimeout(searchTimeout);
757
+ searchTimeout = setTimeout(() => {
758
+ fetchRepositories(term);
759
+ }, 500);
760
+ });
761
+ }
762
+
763
+ function updateActiveItem(items) {
764
+ items.forEach((item, index) => {
765
+ item.classList.toggle('active', index === activeIndex);
766
+ if (index === activeIndex) {
767
+ item.scrollIntoView({ block: 'nearest' });
768
+ }
769
+ });
770
+ }
771
+
772
+ document.addEventListener('click', (e) => {
773
+ if (resultsContainer && !resultsContainer.contains(e.target) && e.target !== repoSearch) {
774
+ resultsContainer.classList.add('hidden');
775
+ }
776
+ });
777
+
778
+ ['INCLUDE_PUBLIC', 'INCLUDE_NOT_OWNED'].forEach(id => {
779
+ const input = document.getElementById(id);
780
+ if (input) {
781
+ input.addEventListener('change', async () => {
782
+ const config = {};
783
+ fields.forEach(f => {
784
+ const inp = document.getElementById(f.id);
785
+ if (inp) {
786
+ if (f.type === 'checkbox' || f.type === 'slider') config[f.id] = inp.checked;
787
+ else config[f.id] = inp.value;
788
+ }
789
+ });
790
+
791
+ try {
792
+ await fetch(api('api/config'), {
793
+ method: 'POST',
794
+ headers: { 'Content-Type': 'application/json' },
795
+ body: JSON.stringify(config)
796
+ });
797
+ await fetchRepositories(repoSearch?.value || '');
798
+ } catch (e) {
799
+ console.error('Erreur lors de la mise à jour auto:', e);
800
+ }
801
+ });
802
+ }
803
+ });
804
+
805
+ if (targetBranchSelect) {
806
+ targetBranchSelect.addEventListener('change', () => {
807
+ const customInput = document.getElementById('TARGET_BRANCH_custom');
808
+ if (!customInput) return;
809
+
810
+ const isNewBranch = targetBranchSelect.value === '';
811
+ customInput.classList.toggle('hidden', !isNewBranch);
812
+ if (isNewBranch) {
813
+ customInput.focus();
814
+ } else {
815
+ customInput.value = '';
816
+ }
817
+ });
818
+ }
819
+ }
820
+
821
+ function showStatus(message, type) {
822
+ const container = document.getElementById('toast-container');
823
+ const toast = document.createElement('div');
824
+
825
+ const bgColor = type === 'success' ? 'bg-green-900/90' : 'bg-red-900/90';
826
+ const borderColor = type === 'success' ? 'border-green-500' : 'border-red-500';
827
+ const textColor = type === 'success' ? 'text-green-100' : 'text-red-100';
828
+ const icon = type === 'success'
829
+ ? '<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>'
830
+ : '<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>';
831
+
832
+ toast.className = `toast flex items-center p-4 rounded-lg border shadow-xl ${bgColor} ${borderColor} ${textColor}`;
833
+ toast.innerHTML = `${icon}<span class="flex-1">${message}</span>`;
834
+
835
+ container.appendChild(toast);
836
+
837
+ // Supprimer le toast après l'animation (5s total)
838
+ setTimeout(() => {
839
+ toast.remove();
840
+ }, 5000);
841
+ }
842
+
843
+ // Uniformisation: addEventListener au lieu de onsubmit/onclick
844
+ form.addEventListener('submit', async (e) => {
845
+ e.preventDefault();
846
+
847
+ // Check if a configuration is selected
848
+ if (!currentConfigName) {
849
+ showStatus('Veuillez sélectionner ou créer une configuration', 'error');
850
+ return;
851
+ }
852
+
853
+ const config = {};
854
+ fields.forEach(field => {
855
+ const input = document.getElementById(field.id);
856
+ if (!input) return; // Protection contre les éléments manquants
857
+ if (field.type === 'checkbox' || field.type === 'slider') {
858
+ config[field.id] = input.checked;
859
+ } else if (field.id === 'TARGET_BRANCH') {
860
+ const customInput = document.getElementById(`${field.id}_custom`);
861
+ config[field.id] = customInput && customInput.value ? customInput.value : input.value;
862
+ } else {
863
+ config[field.id] = input.value;
864
+ }
865
+ });
866
+
867
+ try {
868
+ // Save to configs.json via the new endpoint
869
+ const response = await fetch(api('api/configs'), {
870
+ method: 'POST',
871
+ headers: { 'Content-Type': 'application/json' },
872
+ body: JSON.stringify({ name: currentConfigName, config })
873
+ });
874
+
875
+ if (response.ok) {
876
+ showStatus(`Configuration "${currentConfigName}" enregistrée avec succès !`, 'success');
877
+ } else {
878
+ const data = await response.json().catch(() => ({ error: 'Erreur inconnue' }));
879
+ throw new Error(data.error || 'Erreur lors de l\'enregistrement');
880
+ }
881
+ } catch (error) {
882
+ showStatus(error.message, 'error');
883
+ }
884
+ });
885
+
886
+ restartBtn.addEventListener('click', async () => {
887
+ if (!confirm('Êtes-vous sûr de vouloir redémarrer le serveur MCP ? Cela coupera la connexion actuelle pour appliquer les changements.')) {
888
+ return;
889
+ }
890
+
891
+ const originalHTML = restartBtn.innerHTML; // Sauvegarder le HTML original
892
+ try {
893
+ restartBtn.disabled = true;
894
+ restartBtn.innerHTML = '<span>Redémarrage en cours...</span>';
895
+
896
+ const response = await fetch(api('api/restart'), { method: 'POST' });
897
+
898
+ if (response.ok) {
899
+ showStatus('Signal de redémarrage envoyé. Le serveur va se relancer...', 'success');
900
+ setTimeout(() => window.location.reload(), 3000);
901
+ } else {
902
+ throw new Error('Erreur lors de la demande de redémarrage');
903
+ }
904
+ } catch (error) {
905
+ showStatus(error.message, 'error');
906
+ restartBtn.disabled = false;
907
+ restartBtn.innerHTML = originalHTML; // Restaurer le HTML original
908
+ }
909
+ });
910
+
911
+ stopBtn.addEventListener('click', async () => {
912
+ if (!confirm('Êtes-vous sûr de vouloir arrêter COMPLÈTEMENT le serveur MCP ?')) {
913
+ return;
914
+ }
915
+
916
+ const originalHTML = stopBtn.innerHTML; // Sauvegarder le HTML original
917
+ try {
918
+ stopBtn.disabled = true;
919
+ stopBtn.innerHTML = '<span>Arrêt en cours...</span>';
920
+
921
+ const response = await fetch(api('api/stop'), { method: 'POST' });
922
+
923
+ if (response.ok) {
924
+ showStatus('Le serveur s\'arrête. Cette page ne sera plus disponible.', 'success');
925
+ } else {
926
+ throw new Error('Erreur lors de la demande d\'arrêt');
927
+ }
928
+ } catch (error) {
929
+ showStatus(error.message, 'error');
930
+ stopBtn.disabled = false;
931
+ stopBtn.innerHTML = originalHTML; // Restaurer le HTML original
932
+ }
933
+ });
934
+
935
+ deleteWorkingDirBtn.addEventListener('click', async () => {
936
+ if (!confirm('Êtes-vous sûr de vouloir SUPPRIMER physiquement le dossier de travail local ? Cette action est irréversible et vous devrez effectuer un nouveau "clone" pour utiliser les outils Git.')) {
937
+ return;
938
+ }
939
+
940
+ const originalHTML = deleteWorkingDirBtn.innerHTML;
941
+ try {
942
+ deleteWorkingDirBtn.disabled = true;
943
+ deleteWorkingDirBtn.innerHTML = '<span>Suppression...</span>';
944
+
945
+ const response = await fetch(api('api/delete-working-dir'), { method: 'POST' });
946
+
947
+ if (response.ok) {
948
+ showStatus('Dossier de travail supprimé avec succès.', 'success');
949
+ currentConfig.REPO_PATH = '';
950
+ deleteWorkingDirBtn.disabled = true;
951
+ } else {
952
+ const data = await response.json().catch(() => ({ error: 'Erreur lors de la suppression' }));
953
+ throw new Error(data.error || 'Erreur lors de la suppression');
954
+ }
955
+ } catch (error) {
956
+ showStatus(error.message, 'error');
957
+ } finally {
958
+ deleteWorkingDirBtn.innerHTML = originalHTML;
959
+ deleteWorkingDirBtn.disabled = !currentConfig.REPO_PATH;
960
+ }
961
+ });
962
+
963
+ cloneBtn.addEventListener('click', async () => {
964
+ if (!confirm('Voulez-vous lancer le clonage du dépôt ? Assurez-vous d\'avoir enregistré la configuration (Parent Projects Path) avant.')) {
965
+ return;
966
+ }
967
+
968
+ const originalHTML = cloneBtn.innerHTML;
969
+ try {
970
+ cloneBtn.disabled = true;
971
+ cloneBtn.innerHTML = '<span>Clonage...</span>';
972
+
973
+ const response = await fetch(api('api/clone'), { method: 'POST' });
974
+
975
+ if (response.ok) {
976
+ showStatus('Le processus de clonage a été lancé en arrière-plan. Vérifiez les logs pour le résultat.', 'success');
977
+ } else {
978
+ const data = await response.json().catch(() => ({ error: 'Erreur lors du clonage' }));
979
+ throw new Error(data.error || 'Erreur lors du clonage');
980
+ }
981
+ } catch (error) {
982
+ showStatus(error.message, 'error');
983
+ cloneBtn.disabled = false;
984
+ } finally {
985
+ cloneBtn.innerHTML = originalHTML;
986
+ }
987
+ });
988
+
989
+ fetchConfig();
990
+ </script>
991
+ </body>
992
+ </html>