imcp 0.0.1 → 0.0.3

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.
Files changed (50) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/commands/install.js +8 -8
  3. package/dist/cli/index.js +3 -2
  4. package/dist/core/ConfigurationProvider.d.ts +2 -0
  5. package/dist/core/ConfigurationProvider.js +49 -3
  6. package/dist/core/InstallationService.d.ts +8 -0
  7. package/dist/core/InstallationService.js +117 -0
  8. package/dist/core/MCPManager.d.ts +1 -0
  9. package/dist/core/MCPManager.js +42 -0
  10. package/dist/core/RequirementService.d.ts +7 -0
  11. package/dist/core/RequirementService.js +17 -0
  12. package/dist/core/constants.d.ts +5 -0
  13. package/dist/core/constants.js +29 -6
  14. package/dist/core/installers/BaseInstaller.js +26 -9
  15. package/dist/core/installers/ClientInstaller.d.ts +6 -0
  16. package/dist/core/installers/ClientInstaller.js +149 -12
  17. package/dist/core/installers/GeneralInstaller.js +0 -5
  18. package/dist/core/installers/NpmInstaller.js +2 -1
  19. package/dist/core/types.d.ts +7 -6
  20. package/dist/services/ServerService.js +16 -0
  21. package/dist/utils/clientUtils.js +3 -1
  22. package/dist/utils/versionUtils.d.ts +12 -0
  23. package/dist/utils/versionUtils.js +26 -0
  24. package/dist/web/public/css/modal.css +89 -9
  25. package/dist/web/public/index.html +12 -6
  26. package/dist/web/public/js/modal.js +357 -97
  27. package/dist/web/public/modal.html +20 -11
  28. package/dist/web/server.d.ts +6 -0
  29. package/dist/web/server.js +6 -1
  30. package/package.json +1 -1
  31. package/src/cli/commands/install.ts +11 -14
  32. package/src/cli/index.ts +4 -2
  33. package/src/core/ConfigurationProvider.ts +51 -3
  34. package/src/core/InstallationService.ts +131 -0
  35. package/src/core/MCPManager.ts +60 -1
  36. package/src/core/RequirementService.ts +21 -1
  37. package/src/core/constants.ts +32 -7
  38. package/src/core/installers/BaseInstaller.ts +33 -17
  39. package/src/core/installers/ClientInstaller.ts +148 -12
  40. package/src/core/installers/GeneralInstaller.ts +0 -5
  41. package/src/core/installers/NpmInstaller.ts +2 -1
  42. package/src/core/types.ts +8 -6
  43. package/src/services/ServerService.ts +22 -0
  44. package/src/utils/clientUtils.ts +3 -1
  45. package/src/utils/versionUtils.ts +29 -0
  46. package/src/web/public/css/modal.css +89 -9
  47. package/src/web/public/index.html +12 -6
  48. package/src/web/public/js/modal.js +357 -97
  49. package/src/web/public/modal.html +20 -11
  50. package/src/web/server.ts +16 -2
@@ -4,43 +4,142 @@ import { showToast, showConfirm } from './notifications.js';
4
4
  document.addEventListener('DOMContentLoaded', () => {
5
5
  setupModalOutsideClick();
6
6
  });
7
- /** Delayed message queue for loading modal */
7
+ /**
8
+ * Simple version comparison function
9
+ * @param {string} v1 First version
10
+ * @param {string} v2 Second version
11
+ * @returns {number} 1 if v1 > v2, -1 if v1 < v2, 0 if equal
12
+ */
13
+ function compareVersions(v1, v2) {
14
+ const v1Parts = v1.split('.').map(Number);
15
+ const v2Parts = v2.split('.').map(Number);
16
+
17
+ for (let i = 0; i < Math.max(v1Parts.length, v2Parts.length); i++) {
18
+ const v1Part = v1Parts[i] || 0;
19
+ const v2Part = v2Parts[i] || 0;
20
+
21
+ if (v1Part > v2Part) return 1;
22
+ if (v1Part < v2Part) return -1;
23
+ }
24
+
25
+ return 0;
26
+ }
27
+
28
+ /**
29
+ * Message queue for delayed loading modal updates.
30
+ * @type {Array<{msg: string, isCompletion: boolean}>}
31
+ */
8
32
  let messageQueue = [];
33
+
34
+ /** @type {boolean} Flag indicating if a message is currently being appended */
9
35
  let isAppending = false;
36
+
37
+ /** @type {boolean} Flag indicating if completion UI update is pending */
38
+ let completionPending = false;
39
+
40
+ /**
41
+ * Process messages in the queue with a delay between each message.
42
+ * When the queue is empty and completion is pending, triggers the completion UI.
43
+ * @private
44
+ */
10
45
  function processMessageQueue() {
11
- if (isAppending || messageQueue.length === 0) return;
46
+ if (isAppending || messageQueue.length === 0) {
47
+ // Only update completion UI if all messages have been displayed
48
+ if (messageQueue.length === 0 && completionPending) {
49
+ // Add small delay before showing completion to ensure last message is visible
50
+ setTimeout(() => {
51
+ updateCompletionUI();
52
+ completionPending = false;
53
+ }, 500);
54
+ }
55
+ return;
56
+ }
57
+
12
58
  isAppending = true;
13
- const { msg } = messageQueue.shift();
59
+ const { msg, isCompletion } = messageQueue.shift();
60
+
61
+ if (isCompletion) {
62
+ completionPending = true;
63
+ }
64
+
14
65
  _appendInstallLoadingMessage(msg);
66
+
15
67
  setTimeout(() => {
16
68
  isAppending = false;
17
69
  processMessageQueue();
18
70
  }, 1000);
19
71
  }
20
- // Always use this to append messages with delay
21
- function delayedAppendInstallLoadingMessage(msg) {
22
- messageQueue.push({ msg });
72
+
73
+ /**
74
+ * Queue a message to be displayed with a delay.
75
+ * Messages are displayed sequentially with a 1-second delay between each.
76
+ * @param {string} msg The message to display
77
+ * @param {boolean} [isCompletion=false] If true, triggers completion UI after message display
78
+ */
79
+ function delayedAppendInstallLoadingMessage(msg, isCompletion = false) {
80
+ messageQueue.push({ msg, isCompletion });
23
81
  processMessageQueue();
24
82
  }
25
- // Internal: the original append function
83
+
84
+ function updateCompletionUI() {
85
+ const loadingTitle = document.querySelector('.loading-title');
86
+ const loadingIcon = document.querySelector('.loading-icon');
87
+
88
+ if (loadingTitle) {
89
+ loadingTitle.textContent = 'Completed';
90
+ loadingTitle.classList.add('completed');
91
+ }
92
+
93
+ if (loadingIcon) {
94
+ loadingIcon.innerHTML = `
95
+ <svg width="48" height="48" viewBox="0 0 48 48" fill="none">
96
+ <circle cx="24" cy="24" r="20" stroke="#059669" stroke-width="4"/>
97
+ <path d="M16 24l6 6 12-12" stroke="#059669" stroke-width="4" stroke-linecap="round" stroke-linejoin="round"/>
98
+ </svg>
99
+ `;
100
+ loadingIcon.classList.add('completed');
101
+ }
102
+ }
103
+ /**
104
+ * Internal function to append a message to the loading modal with formatting.
105
+ * @private
106
+ * @param {string} message - The message to append
107
+ */
26
108
  function _appendInstallLoadingMessage(message) {
27
109
  const loadingMsg = document.getElementById('installLoadingMessage');
28
110
  if (loadingMsg) {
29
- loadingMsg.innerHTML += `<div>${message}</div>`;
30
- loadingMsg.scrollTop = loadingMsg.scrollHeight;
111
+ // Split message on newlines
112
+ const lines = message.split('\n');
113
+
114
+ lines.forEach(line => {
115
+ // Check if line contains an error
116
+ const isError = line.toLowerCase().includes('error') ||
117
+ line.toLowerCase().includes('failed') ||
118
+ line.toLowerCase().includes('timeout');
119
+
120
+ const formattedMessage = line;
121
+
122
+ // Create message container
123
+ const messageDiv = document.createElement('div');
124
+ messageDiv.className = 'message-line';
125
+
126
+ // Apply yellow color for error messages
127
+ if (isError) {
128
+ messageDiv.style.color = '#f59e0b';
129
+ }
130
+ messageDiv.innerHTML = formattedMessage;
131
+
132
+
133
+ loadingMsg.appendChild(messageDiv);
134
+ loadingMsg.scrollTop = loadingMsg.scrollHeight;
135
+ });
31
136
  } else {
32
- console.error('[LoadingModal] loading message DOM not found');
137
+ console.error('[LoadingModal] Element not found: installLoadingMessage');
33
138
  }
34
139
  }
35
140
 
36
141
  /**
37
- * Show the install loading modal and optionally append a message.
38
- * @param {string} message
39
- */
40
-
41
- /**
42
- * Show the install loading modal and optionally append a message.
43
- * @param {string} message
142
+ * Display the installation loading modal and prepare it for messages.
44
143
  */
45
144
  function showInstallLoadingModal() {
46
145
  const loadingModal = document.getElementById('installLoadingModal');
@@ -49,33 +148,30 @@ function showInstallLoadingModal() {
49
148
  loadingModal.style.display = 'block';
50
149
  loadingMsg.innerHTML = '';
51
150
  } else {
52
- console.error('[LoadingModal] loading modal DOM not found');
151
+ console.error('[LoadingModal] Required elements not found: installLoadingModal or installLoadingMessage');
53
152
  }
54
153
  }
55
154
 
56
155
  /**
57
- * Append a message to the loading modal.
58
- * @param {string} message
156
+ * Append a message to the loading modal display.
157
+ * @param {string} message - The message to display
59
158
  */
60
159
  function appendInstallLoadingMessage(message) {
61
- console.log('[LoadingModal] appendInstallLoadingMessage:', message);
62
- const loadingMsg = document.getElementById('installLoadingMessage');
63
- if (loadingMsg) {
64
- loadingMsg.innerHTML += `<div>${message}</div>`;
65
- loadingMsg.scrollTop = loadingMsg.scrollHeight;
66
- } else {
67
- console.error('[LoadingModal] loading message DOM not found');
68
- }
160
+ console.log(`[LoadingModal] Message: ${message}`);
161
+ _appendInstallLoadingMessage(message);
69
162
  }
70
163
 
71
164
  /**
72
165
  * Hide the install loading modal.
73
166
  */
74
167
  function hideInstallLoadingModal() {
75
- console.log('[LoadingModal] hideInstallLoadingModal called');
168
+ console.log('[LoadingModal] Hiding installation modal');
76
169
  const loadingModal = document.getElementById('installLoadingModal');
77
170
  if (loadingModal) {
78
171
  loadingModal.style.display = 'none';
172
+ setTimeout(() => {
173
+ location.reload();
174
+ }, 100);
79
175
  } else {
80
176
  console.error('[LoadingModal] loading modal DOM not found');
81
177
  }
@@ -147,8 +243,8 @@ async function showInstallModal(categoryName, serverName, callback) {
147
243
  const operationStatus = serverStatus.installedStatus[target] || { status: 'not-installed', type: 'check', target: 'server' };
148
244
 
149
245
  // Determine client status
150
- let statusText = 'Not Installed';
151
- let statusClass = 'not-installed';
246
+ let statusText = '';
247
+ let statusClass = '';
152
248
 
153
249
  if (operationStatus.status === 'completed' && operationStatus.type === 'install') {
154
250
  statusText = 'Installed';
@@ -163,6 +259,7 @@ async function showInstallModal(categoryName, serverName, callback) {
163
259
  statusText = 'Failed';
164
260
  statusClass = 'not-installed';
165
261
  }
262
+ // Do not show not-installed status for targets that aren't installed
166
263
 
167
264
  const isConfigured = operationStatus.status === 'completed' && operationStatus.type === 'install';
168
265
 
@@ -222,14 +319,16 @@ async function showInstallModal(categoryName, serverName, callback) {
222
319
  // Add elements to client info
223
320
  clientInfo.appendChild(clientName);
224
321
 
225
- // Status badge
226
- const statusBadge = document.createElement('span');
227
- statusBadge.className = `status-badge ${statusClass}`;
228
- statusBadge.textContent = statusText;
229
-
230
- // Add components to client item
322
+ // Add client info (name) to the item first
231
323
  clientItem.appendChild(clientInfo);
232
- clientItem.appendChild(statusBadge);
324
+
325
+ // Status badge - only show if we have status text
326
+ if (statusText) {
327
+ const statusBadge = document.createElement('span');
328
+ statusBadge.className = `status-badge ${statusClass}`;
329
+ statusBadge.textContent = statusText;
330
+ clientItem.appendChild(statusBadge);
331
+ }
233
332
 
234
333
  // Add client item to target div
235
334
  targetDiv.appendChild(clientItem);
@@ -260,6 +359,9 @@ async function showInstallModal(categoryName, serverName, callback) {
260
359
  noEnvMessage.textContent = 'No environment variables required for this MCP server.';
261
360
  envInputsDiv.appendChild(noEnvMessage);
262
361
  } else {
362
+ // Get clientMcpSettings from the targets endpoint
363
+ let clientSettings = targetData.clientMcpSettings;
364
+
263
365
  // Create inputs for each environment variable
264
366
  Object.keys(envRequirements).forEach(key => {
265
367
  const req = envRequirements[key];
@@ -277,7 +379,21 @@ async function showInstallModal(categoryName, serverName, callback) {
277
379
  input.id = inputId;
278
380
  input.name = key;
279
381
  input.placeholder = req.Description || key;
280
- input.value = req.Default || '';
382
+
383
+ // For default value, first check MSRooCode for the target server, then use the provided default
384
+ let defaultValue = req.Default || '';
385
+
386
+ // Check if we have settings from MSRooCode
387
+ if (clientSettings && clientSettings.MSRooCode &&
388
+ clientSettings.MSRooCode.mcpServers &&
389
+ clientSettings.MSRooCode.mcpServers[serverName] &&
390
+ clientSettings.MSRooCode.mcpServers[serverName].env &&
391
+ clientSettings.MSRooCode.mcpServers[serverName].env[key]) {
392
+ defaultValue = clientSettings.MSRooCode.mcpServers[serverName].env[key];
393
+ console.log(`Using MSRooCode value for ${key}: ${defaultValue}`);
394
+ }
395
+
396
+ input.value = defaultValue;
281
397
  input.required = req.Required;
282
398
  input.className = 'input-field';
283
399
 
@@ -302,10 +418,36 @@ async function showInstallModal(categoryName, serverName, callback) {
302
418
  if (serverRequirements.length > 0) {
303
419
  const reqHtml = serverRequirements.map(req => {
304
420
  const status = requirements[req.name] || {};
305
- const statusClass = status.installed
421
+ let statusClass = status.installed
306
422
  ? 'text-green-600 bg-green-50'
307
423
  : 'text-yellow-600 bg-yellow-50';
308
- const statusText = status.installed ? 'Installed' : 'Required';
424
+ let statusText = status.installed ? 'Installed' : 'Required';
425
+ let versionDisplay = status.version ? ` • <span class="font-medium">${status.version}</span>` : '';
426
+ let updateToggle = '';
427
+
428
+ // Check if there's an available update
429
+ if (status.installed && status.availableUpdate && status.availableUpdate.version) {
430
+ if (status.version && compareVersions(status.availableUpdate.version, status.version) > 0) {
431
+ // Show version update information with yellow color and icon
432
+ statusClass = 'text-yellow-600 bg-yellow-50';
433
+ statusText = `<span style="color: #f59e0b; font-weight: bold; margin-right: 5px;">↑</span>${status.availableUpdate.version}`;
434
+
435
+ // Create a toggle switch for update
436
+ updateToggle = `
437
+ <label class="inline-flex items-center cursor-pointer ml-2">
438
+ <input type="checkbox" class="toggle-update sr-only"
439
+ data-name="${req.name}"
440
+ data-version="${status.availableUpdate.version}"
441
+ data-category="${categoryName}"
442
+ data-server="${serverName}">
443
+ <div class="relative w-10 h-5 bg-gray-200 rounded-full toggle-bg">
444
+ <div class="absolute inset-y-0 left-0 w-5 h-5 bg-white rounded-full transition-transform duration-300 transform"></div>
445
+ </div>
446
+ <span class="ml-2 text-sm text-gray-700">Update</span>
447
+ </label>
448
+ `;
449
+ }
450
+ }
309
451
 
310
452
  return `
311
453
  <div class="border border-gray-200 p-3 rounded-lg mb-2 hover:bg-gray-50">
@@ -313,12 +455,15 @@ async function showInstallModal(categoryName, serverName, callback) {
313
455
  <div>
314
456
  <div class="font-semibold text-gray-800">${req.name}</div>
315
457
  <div class="text-sm text-gray-600 shadow-sm p-1 rounded bg-gray-50">
316
- <span class="font-medium">${status.type || 'package'}</span>${status.version ? ` • <span class="font-medium">${status.version}</span>` : ''}
458
+ <span class="font-medium">${status.type || 'package'}</span>${versionDisplay}
317
459
  </div>
318
460
  </div>
319
- <span class="${statusClass} inline-flex items-center px-3 py-1 rounded-full text-sm">
320
- ${statusText}
321
- </span>
461
+ <div class="flex items-center">
462
+ <span class="${statusClass} inline-flex items-center px-3 py-1 rounded-full text-sm">
463
+ ${statusText}
464
+ </span>
465
+ ${updateToggle}
466
+ </div>
322
467
  </div>
323
468
  </div>
324
469
  `;
@@ -333,6 +478,24 @@ async function showInstallModal(categoryName, serverName, callback) {
333
478
  modalRequirements.innerHTML = '<p class="text-gray-600">No additional dependencies required.</p>';
334
479
  }
335
480
 
481
+ // Add event listeners for update toggles
482
+ setTimeout(() => {
483
+ const updateToggles = modalRequirements.querySelectorAll('.toggle-update');
484
+ updateToggles.forEach(toggle => {
485
+ toggle.addEventListener('change', function () {
486
+ // When toggled, update the visual appearance
487
+ const toggleBg = this.parentElement.querySelector('.toggle-bg');
488
+ if (this.checked) {
489
+ toggleBg.classList.add('bg-blue-500');
490
+ toggleBg.querySelector('div').classList.add('translate-x-5');
491
+ } else {
492
+ toggleBg.classList.remove('bg-blue-500');
493
+ toggleBg.querySelector('div').classList.remove('translate-x-5');
494
+ }
495
+ });
496
+ });
497
+ }, 100);
498
+
336
499
  // Set up the install form submit handler
337
500
  const installForm = document.getElementById('installForm');
338
501
  installForm.onsubmit = (e) => {
@@ -347,13 +510,27 @@ async function showInstallModal(categoryName, serverName, callback) {
347
510
  }
348
511
  });
349
512
 
513
+ // Check for enabled update toggles and collect requirements to update
514
+ const requirementsToUpdate = [];
515
+ const updateToggles = modalRequirements.querySelectorAll('.toggle-update:checked');
516
+ updateToggles.forEach(toggle => {
517
+ requirementsToUpdate.push({
518
+ name: toggle.dataset.name,
519
+ version: toggle.dataset.version
520
+ });
521
+ });
522
+
350
523
  // Get selected clients
351
524
  const selectedTargets = window.selectedClients.length > 0 ?
352
525
  window.selectedClients :
353
526
  Array.from(document.querySelectorAll('.client-item.selected'))
354
527
  .map(item => item.dataset.target);
355
528
 
356
- if (selectedTargets.length === 0) {
529
+ // Check if we have any requirements selected for update
530
+ const hasRequirementsToUpdate = requirementsToUpdate.length > 0;
531
+
532
+ // Only require client selection if we don't have any requirements to update
533
+ if (selectedTargets.length === 0 && !hasRequirementsToUpdate) {
357
534
  showToast('Please select at least one client to configure.', 'error');
358
535
  return;
359
536
  }
@@ -367,7 +544,19 @@ async function showInstallModal(categoryName, serverName, callback) {
367
544
  const msg = serverStatus.installedStatus?.[target]?.message;
368
545
  if (msg) installingMessage = msg;
369
546
  }
370
- handleBulkClientInstall(categoryName, serverName, selectedTargets, envVars, installingMessage, serverData);
547
+
548
+ // Add requirements to update to serverInstallOptions if any
549
+ const serverInstallOptions = {
550
+ targetClients: selectedTargets,
551
+ env: envVars
552
+ };
553
+
554
+ // Only add requirements if we have any to update
555
+ if (requirementsToUpdate.length > 0) {
556
+ serverInstallOptions.requirements = requirementsToUpdate;
557
+ }
558
+
559
+ handleBulkClientInstall(categoryName, serverName, selectedTargets, envVars, installingMessage, serverData, serverInstallOptions);
371
560
  };
372
561
 
373
562
  } catch (error) {
@@ -379,8 +568,8 @@ async function showInstallModal(categoryName, serverName, callback) {
379
568
  }
380
569
 
381
570
  // Function to handle bulk client installations
382
- async function handleBulkClientInstall(categoryName, serverName, targets, envVars = {}, installingMessage = "Starting installation...", serverData = null) {
383
- console.log('[LoadingModal] handleBulkClientInstall called', { categoryName, serverName, targets, envVars });
571
+ async function handleBulkClientInstall(categoryName, serverName, targets, envVars = {}, installingMessage = "Starting installation...", serverData = null, serverInstallOptions = null) {
572
+ console.log('[LoadingModal] handleBulkClientInstall called', { categoryName, serverName, targets, envVars, serverInstallOptions });
384
573
  // Hide install modal, show loading modal
385
574
  const installModal = document.getElementById('installModal');
386
575
  console.log('[LoadingModal] installModal:', installModal);
@@ -401,48 +590,52 @@ async function handleBulkClientInstall(categoryName, serverName, targets, envVar
401
590
  }
402
591
  }
403
592
 
404
- showInstallLoadingModal(installingMessage);
405
-
593
+ showInstallLoadingModal();
406
594
  delayedAppendInstallLoadingMessage(installingMessage);
407
595
 
408
596
  try {
409
597
  delayedAppendInstallLoadingMessage("Installing, please wait...");
598
+
599
+ // Use serverInstallOptions if provided, otherwise build the traditional options
600
+ const requestBody = {
601
+ serverList: {
602
+ [serverName]: serverInstallOptions || {
603
+ targetClients: targets,
604
+ env: envVars
605
+ }
606
+ }
607
+ };
608
+
410
609
  const response = await fetch(`/api/categories/${categoryName}/install`, {
411
610
  method: 'POST',
412
611
  headers: {
413
612
  'Content-Type': 'application/json',
414
613
  'Accept': 'application/json'
415
614
  },
416
- body: JSON.stringify({
417
- serverList: {
418
- [serverName]: {
419
- targetClients: targets,
420
- env: envVars
421
- }
422
- }
423
- })
615
+ body: JSON.stringify(requestBody)
424
616
  });
425
617
 
426
618
  console.log('[LoadingModal] fetch install response:', response);
427
619
 
428
620
  if (!response.ok) {
429
621
  const errorData = await response.text();
430
- delayedAppendInstallLoadingMessage(`<span style="color:red;">Installation failed: ${errorData || response.statusText}</span>`);
431
- console.error('[LoadingModal] Installation failed:', errorData || response.statusText);
622
+ delayedAppendInstallLoadingMessage(`<span style="color:#f59e0b;">Failed: ${errorData || response.statusText}</span>`);
623
+ console.error('[LoadingModal] Failed:', errorData || response.statusText);
432
624
  throw new Error(`Installation failed: ${errorData || response.statusText}`);
433
625
  }
434
626
 
435
627
  const result = await response.json();
436
628
  console.log('[LoadingModal] install result:', result);
437
629
  if (!result.success) {
438
- delayedAppendInstallLoadingMessage(`<span style="color:red;">${result.error || 'Installation failed'}</span>`);
439
- console.error('[LoadingModal] install result error:', result.error || 'Installation failed');
630
+ delayedAppendInstallLoadingMessage(`<span style="color:#f59e0b;">${result.error || 'Operation failed'}</span>`);
631
+ console.error('[LoadingModal] Error:', result.error || 'Operation failed');
440
632
  throw new Error(result.error || 'Installation failed');
441
633
  }
442
634
 
443
635
  // Optionally, you can refresh modal data here or trigger a callback
444
- // Start polling for install status
445
- pollInstallStatus(categoryName, serverName, targets);
636
+ // Start polling for install status, pass requirements if available
637
+ const requirements = serverInstallOptions?.requirements || [];
638
+ pollInstallStatus(categoryName, serverName, targets, 2000, 60, requirements);
446
639
  } catch (error) {
447
640
  console.error('[LoadingModal] Error applying configuration:', error);
448
641
  delayedAppendInstallLoadingMessage(`<span style="color:red;">Error: ${error.message}</span>`);
@@ -455,53 +648,101 @@ async function handleBulkClientInstall(categoryName, serverName, targets, envVar
455
648
  }
456
649
  }
457
650
 
458
- // Poll install status for the given server/targets
459
- async function pollInstallStatus(categoryName, serverName, targets, interval = 2000, maxTries = 60) {
651
+ // Poll install status for the given server/targets and optional requirements
652
+ async function pollInstallStatus(categoryName, serverName, targets, interval = 2000, maxTries = 60, requirements = []) {
460
653
  let tries = 0;
461
654
  let lastMessages = {};
462
- // Use global delayedAppendInstallLoadingMessage and queue logic
655
+ let lastRequirementMessages = {};
656
+ let completionMessageSent = false;
657
+
463
658
  while (tries < maxTries) {
464
659
  try {
465
660
  const resp = await fetch(`/api/categories/${categoryName}`);
466
661
  if (resp.ok) {
467
662
  const data = await resp.json();
468
- const serverStatuses = data?.data?.installationStatus?.serversStatus || {};
663
+ const installationStatus = data?.data?.installationStatus || {};
664
+ const serverStatuses = installationStatus.serversStatus || {};
665
+ const requirementsStatus = installationStatus.requirementsStatus || {};
469
666
  const serverStatus = serverStatuses[serverName] || { installedStatus: {} };
470
- let allCompleted = true;
471
- for (const target of targets) {
472
- const status = serverStatus.installedStatus?.[target]?.status;
473
- const msg = serverStatus.installedStatus?.[target]?.message;
474
- // Only append new messages
475
- if (msg && lastMessages[target] !== msg) {
476
- delayedAppendInstallLoadingMessage(`[${target}] ${msg}`);
477
- lastMessages[target] = msg;
667
+
668
+ // First check requirements status if we have any
669
+ let allRequirementsCompleted = true;
670
+ let hasRequirements = requirements.length > 0;
671
+
672
+ for (const req of requirements) {
673
+ const reqStatus = requirementsStatus[req.name] || {};
674
+ const opStatus = reqStatus.operationStatus || {};
675
+ const msg = opStatus.message;
676
+ const status = opStatus.status;
677
+
678
+ // Only append new messages for requirements
679
+ if (msg && lastRequirementMessages[req.name] !== msg) {
680
+ delayedAppendInstallLoadingMessage(`${req.name}: ${msg}`);
681
+ lastRequirementMessages[req.name] = msg;
478
682
  }
479
- if (status !== "completed") {
480
- allCompleted = false;
683
+
684
+ if (status !== "completed" && status !== "failed") {
685
+ allRequirementsCompleted = false;
686
+ }
687
+
688
+ // If a requirement failed, show an error
689
+ if (status === "failed") {
690
+ delayedAppendInstallLoadingMessage(`<span style="color:#f59e0b;">${req.name}: Update failed - ${msg || 'Unknown error'}</span>`);
691
+ }
692
+ }
693
+
694
+ // Now check target statuses
695
+ let allTargetsCompleted = true;
696
+ let hasTargets = targets && targets.length > 0;
697
+
698
+ if (hasTargets) {
699
+ for (const target of targets) {
700
+ const status = serverStatus.installedStatus?.[target]?.status;
701
+ const msg = serverStatus.installedStatus?.[target]?.message;
702
+ // Only append new messages for targets, making output more compact
703
+ if (msg && lastMessages[target] !== msg) {
704
+ delayedAppendInstallLoadingMessage(`${target}: ${msg}`);
705
+ lastMessages[target] = msg;
706
+ }
707
+ if (status !== "completed") {
708
+ allTargetsCompleted = false;
709
+ }
481
710
  }
482
711
  }
483
- if (allCompleted) {
484
- delayedAppendInstallLoadingMessage(`<span style="color:green;">Configuration applied successfully for ${targets.length} client(s).</span>`);
485
- setTimeout(() => {
486
- hideInstallLoadingModal();
487
- setTimeout(() => {
488
- location.reload();
489
- }, 200);
490
- }, 5000);
712
+
713
+ // Complete if all operations are done
714
+ const allCompleted = (!hasRequirements || allRequirementsCompleted) &&
715
+ (!hasTargets || allTargetsCompleted);
716
+
717
+ if (allCompleted && !completionMessageSent) {
718
+ completionMessageSent = true;
719
+
720
+ // Compose completion message
721
+ const completionMessage = hasRequirements && hasTargets ?
722
+ `Updated requirements and configured ${targets.length} client(s)` :
723
+ hasRequirements ? 'Requirements updated' :
724
+ hasTargets ? `Configured ${targets.length} client(s)` :
725
+ 'Operation completed';
726
+
727
+ console.log('[LoadingModal] Completed:', completionMessage);
728
+ delayedAppendInstallLoadingMessage(`<span style="color:green;">${completionMessage}</span>`, true);
729
+
730
+ // Give time for message queue to process before returning
731
+ await new Promise(resolve => setTimeout(resolve, 1500));
491
732
  return;
492
733
  }
493
734
  }
494
- } catch (e) {
495
- // Ignore errors and continue polling
735
+ } catch (error) {
736
+ console.error('[LoadingModal] Error polling status:', error);
496
737
  }
497
738
  await new Promise(resolve => setTimeout(resolve, interval));
498
739
  tries++;
499
740
  }
500
- // Timeout: close modal and reload anyway
501
- hideInstallLoadingModal();
502
- setTimeout(() => {
503
- location.reload();
504
- }, 200);
741
+
742
+ // On timeout, show timeout message but don't auto-close
743
+ if (!completionMessageSent) {
744
+ delayedAppendInstallLoadingMessage(`<span style="color:#f59e0b;">Operation timed out - Please refresh the page</span>`, true);
745
+ }
505
746
  }
506
747
 
507
748
  // Function to handle client uninstallation for multiple targets
@@ -545,17 +786,22 @@ async function uninstallTools(categoryName, serverList, targets) {
545
786
  }
546
787
  }
547
788
 
548
- // Close modal
789
+ // Close modal and reload
549
790
  function closeModal() {
550
791
  document.getElementById('installModal').style.display = "none";
792
+ location.reload();
551
793
  }
552
794
 
553
795
  // Close modal if clicked outside content
554
796
  function setupModalOutsideClick() {
555
797
  window.onclick = function (event) {
556
- const modal = document.getElementById('installModal');
557
- if (event.target == modal) {
798
+ const installModal = document.getElementById('installModal');
799
+ const loadingModal = document.getElementById('installLoadingModal');
800
+
801
+ if (event.target == installModal) {
558
802
  closeModal();
803
+ } else if (event.target == loadingModal) {
804
+ hideInstallLoadingModal();
559
805
  }
560
806
  };
561
807
  }
@@ -568,5 +814,19 @@ window.showInstallLoadingModal = showInstallLoadingModal;
568
814
  window.appendInstallLoadingMessage = appendInstallLoadingMessage;
569
815
  window.hideInstallLoadingModal = hideInstallLoadingModal;
570
816
 
817
+ // CSS styles for the toggle switch
818
+ const styleElement = document.createElement('style');
819
+ styleElement.textContent = `
820
+ .toggle-bg.bg-blue-500 {
821
+ background-color: #3b82f6;
822
+ }
823
+ .toggle-bg {
824
+ transition: background-color 0.3s;
825
+ }
826
+ .toggle-bg div {
827
+ transition: transform 0.3s;
828
+ }
829
+ `;
830
+ document.head.appendChild(styleElement);
571
831
 
572
832
  export { showInstallModal, closeModal, setupModalOutsideClick, uninstallTools };