@@ -117,7 +117,8 @@ export async function getServicesStatus(services) {
117117 if ( processStatus . status === 'running' && healthStatus . status === 'down' ) {
118118 combinedStatus . status = 'starting' ; // Process running but not responding yet
119119 } else if ( processStatus . status === 'stopped' && healthStatus . status === 'up' ) {
120- combinedStatus . status = 'external' ; // Running externally (not managed by us)
120+ combinedStatus . status = 'up' ; // Running externally but show as up
121+ combinedStatus . processStatus = 'external' ; // Keep track that it's external
121122 }
122123
123124 return combinedStatus ;
@@ -223,65 +224,208 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
223224 var nextRefreshLabel;
224225
225226 function startService(serviceName) {
227+ // Show loading state
228+ updateButtonState(serviceName, 'starting');
229+
226230 fetch('/api/services/start', {
227231 method: 'POST',
228232 headers: { 'Content-Type': 'application/json' },
229233 body: JSON.stringify({ serviceName: serviceName })
230234 })
231- .then(function(response) { return response.json(); })
235+ .then(function(response) {
236+ if (!response.ok) {
237+ throw new Error('Network error: ' + response.status);
238+ }
239+ return response.json();
240+ })
232241 .then(function(result) {
233242 if (result.error) {
234- alert('Failed to start ' + serviceName + ': ' + result.error);
243+ showUserFriendlyError(' start', serviceName, result.error);
235244 } else {
236- alert ('Started ' + serviceName + ' successfully' );
245+ showUserFriendlySuccess ('Started', serviceName);
237246 setTimeout(fetchStatus, 1000);
238247 }
239248 })
240249 .catch(function(error) {
241- alert('Error starting ' + serviceName + ': ' + error.message);
250+ showUserFriendlyError('start', serviceName, error.message);
251+ })
252+ .finally(function() {
253+ // Reset button state after operation
254+ setTimeout(fetchStatus, 500);
242255 });
243256 }
244257
245258 function stopService(serviceName) {
259+ // Show loading state
260+ updateButtonState(serviceName, 'stopping');
261+
246262 fetch('/api/services/stop', {
247263 method: 'POST',
248264 headers: { 'Content-Type': 'application/json' },
249265 body: JSON.stringify({ serviceName: serviceName })
250266 })
251- .then(function(response) { return response.json(); })
267+ .then(function(response) {
268+ if (!response.ok) {
269+ throw new Error('Network error: ' + response.status);
270+ }
271+ return response.json();
272+ })
252273 .then(function(result) {
253274 if (result.error) {
254- alert('Failed to stop ' + serviceName + ': ' + result.error);
275+ showUserFriendlyError(' stop', serviceName, result.error);
255276 } else {
256- alert ('Stopped ' + serviceName + ' successfully' );
277+ showUserFriendlySuccess ('Stopped', serviceName);
257278 setTimeout(fetchStatus, 1000);
258279 }
259280 })
260281 .catch(function(error) {
261- alert('Error stopping ' + serviceName + ': ' + error.message);
282+ showUserFriendlyError('stop', serviceName, error.message);
283+ })
284+ .finally(function() {
285+ // Reset button state after operation
286+ setTimeout(fetchStatus, 500);
262287 });
263288 }
264289
265290 function restartService(serviceName) {
291+ // Show loading state
292+ updateButtonState(serviceName, 'restarting');
293+
266294 fetch('/api/services/restart', {
267295 method: 'POST',
268296 headers: { 'Content-Type': 'application/json' },
269297 body: JSON.stringify({ serviceName: serviceName })
270298 })
271- .then(function(response) { return response.json(); })
299+ .then(function(response) {
300+ if (!response.ok) {
301+ throw new Error('Network error: ' + response.status);
302+ }
303+ return response.json();
304+ })
272305 .then(function(result) {
273306 if (result.error) {
274- alert('Failed to restart ' + serviceName + ': ' + result.error);
307+ showUserFriendlyError(' restart', serviceName, result.error);
275308 } else {
276- alert ('Restarted ' + serviceName + ' successfully' );
309+ showUserFriendlySuccess ('Restarted', serviceName);
277310 setTimeout(fetchStatus, 1000);
278311 }
279312 })
280313 .catch(function(error) {
281- alert('Error restarting ' + serviceName + ': ' + error.message);
314+ showUserFriendlyError('restart', serviceName, error.message);
315+ })
316+ .finally(function() {
317+ // Reset button state after operation
318+ setTimeout(fetchStatus, 1000);
282319 });
283320 }
284321
322+ // Helper functions for better user feedback
323+ function showUserFriendlyError(action, serviceName, errorMessage) {
324+ let userMessage = '';
325+ let suggestions = '';
326+
327+ // Parse common error patterns and provide helpful messages
328+ if (errorMessage.includes('already running')) {
329+ userMessage = serviceName + ' is already running';
330+ suggestions = 'Try refreshing the page or restart the service instead.';
331+ } else if (errorMessage.includes('not running')) {
332+ userMessage = serviceName + ' is not currently running';
333+ suggestions = 'Try starting the service first.';
334+ } else if (errorMessage.includes('Service directory not found')) {
335+ userMessage = serviceName + ' directory not found';
336+ suggestions = 'Check if the service exists in the services/ folder.';
337+ } else if (errorMessage.includes('Unsupported service type')) {
338+ userMessage = serviceName + ' has an unsupported service type';
339+ suggestions = 'This service type cannot be managed through the dashboard.';
340+ } else if (errorMessage.includes('Network error')) {
341+ userMessage = 'Connection problem';
342+ suggestions = 'Check if the admin dashboard is running properly and try again.';
343+ } else if (errorMessage.includes('Port') && errorMessage.includes('in use')) {
344+ userMessage = serviceName + ' cannot start - port is already in use';
345+ suggestions = 'Another process might be using the same port. Check for conflicts.';
346+ } else if (errorMessage.includes('permission') || errorMessage.includes('EACCES')) {
347+ userMessage = 'Permission denied';
348+ suggestions = 'Check file permissions or try running with appropriate privileges.';
349+ } else if (errorMessage.includes('ENOENT') || errorMessage.includes('not found')) {
350+ userMessage = 'Required files or commands not found';
351+ suggestions = 'Make sure all dependencies are installed (npm install, python packages, etc).';
352+ } else {
353+ userMessage = 'Failed to ' + action + ' ' + serviceName;
354+ suggestions = 'Check the service logs for more details.';
355+ }
356+
357+ showNotification('❌ ' + userMessage, suggestions, 'error');
358+ }
359+
360+ function showUserFriendlySuccess(action, serviceName) {
361+ const messages = {
362+ 'Started': '✅ ' + serviceName + ' started successfully',
363+ 'Stopped': '🛑 ' + serviceName + ' stopped successfully',
364+ 'Restarted': '🔄 ' + serviceName + ' restarted successfully'
365+ };
366+ showNotification(messages[action] || action + ' ' + serviceName, '', 'success');
367+ }
368+
369+ function updateButtonState(serviceName, state) {
370+ const row = document.querySelector('#row-' + serviceName);
371+ if (!row) return;
372+
373+ const buttons = row.querySelectorAll('.service-controls button');
374+ buttons.forEach(function(btn) {
375+ if (state === 'starting' && btn.classList.contains('btn-start')) {
376+ btn.disabled = true;
377+ btn.textContent = 'Starting...';
378+ } else if (state === 'stopping' && btn.classList.contains('btn-stop')) {
379+ btn.disabled = true;
380+ btn.textContent = 'Stopping...';
381+ } else if (state === 'restarting' && btn.classList.contains('btn-restart')) {
382+ btn.disabled = true;
383+ btn.textContent = 'Restarting...';
384+ }
385+ });
386+ }
387+
388+ function showNotification(message, suggestion, type) {
389+ // Remove any existing notifications
390+ const existing = document.querySelector('#service-notification');
391+ if (existing) existing.remove();
392+
393+ // Create notification element
394+ const notification = document.createElement('div');
395+ notification.id = 'service-notification';
396+ notification.style.cssText =
397+ 'position: fixed; top: 20px; right: 20px; max-width: 400px; padding: 16px; ' +
398+ 'border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.15); z-index: 1000; ' +
399+ 'font-family: system-ui, -apple-system, sans-serif; line-height: 1.4;' +
400+ (type === 'error'
401+ ? 'background: #fef2f2; border: 1px solid #fecaca; color: #b91c1c;'
402+ : 'background: #f0fdf4; border: 1px solid #bbf7d0; color: #15803d;');
403+
404+ let content = '<div style="font-weight: 600; margin-bottom: 4px;">' + message + '</div>';
405+ if (suggestion) {
406+ content += '<div style="font-size: 0.9em; opacity: 0.8;">' + suggestion + '</div>';
407+ }
408+ notification.innerHTML = content;
409+
410+ document.body.appendChild(notification);
411+
412+ // Auto-hide after 5 seconds
413+ setTimeout(function() {
414+ if (notification.parentNode) {
415+ notification.style.opacity = '0';
416+ notification.style.transform = 'translateX(100%)';
417+ setTimeout(function() { notification.remove(); }, 300);
418+ }
419+ }, 5000);
420+
421+ // Add transition for smooth appearance
422+ notification.style.transform = 'translateX(100%)';
423+ notification.style.transition = 'all 0.3s ease';
424+ setTimeout(function() {
425+ notification.style.transform = 'translateX(0)';
426+ }, 50);
427+ }
428+
285429 function scheduleCountdown() {
286430 const el = document.querySelector('.refresh');
287431 if (!el) return;
@@ -320,7 +464,9 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
320464 // Build rows HTML
321465 const rows = services.map(s => {
322466 const processInfo = s.processStatus && s.processStatus !== 'stopped' ?
323- '<div style="font-size:0.6rem;color:#6b7280;margin-top:2px;">PID: ' + (s.pid || 'N/A') + ' | Uptime: ' + (s.uptime ? Math.floor(s.uptime / 60) + 'm' : '0m') + '</div>' : '';
467+ '<div style="font-size:0.6rem;color:#6b7280;margin-top:2px;">' +
468+ (s.processStatus === 'external' ? 'External Process' : 'PID: ' + (s.pid || 'N/A') + ' | Uptime: ' + (s.uptime ? Math.floor(s.uptime / 60) + 'm' : '0m')) +
469+ '</div>' : '';
324470
325471 return '<tr id="row-'+s.name+'">'
326472 + '<td><strong>'+s.name+'</strong></td>'
@@ -329,9 +475,9 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
329475 + '<td><span class="status-badge '+s.status+'"><span class="dot '+s.status+'" style="box-shadow:none;width:8px;height:8px;"></span>'+s.status.toUpperCase()+'</span>' + processInfo + '</td>'
330476 + '<td><span class="path">'+(s.path || 'services/'+s.name)+'</span></td>'
331477 + '<td><div class="service-controls">'
332- + '<button class="btn-sm btn-start" data-service="'+s.name+'" '+(s.processStatus === 'running' ? 'disabled' : '')+'>Start</button>'
333- + '<button class="btn-sm btn-stop" data-service="'+s.name+'" '+(s.processStatus === 'stopped' ? 'disabled' : '')+'>Stop</button>'
334- + '<button class="btn-sm btn-restart" data-service="'+s.name+'">Restart</button>'
478+ + '<button class="btn-sm btn-start" data-service="'+s.name+'" '+(s.processStatus === 'running' || s.processStatus === 'external' ? 'disabled' : '')+'>Start</button>'
479+ + '<button class="btn-sm btn-stop" data-service="'+s.name+'" '+(s.processStatus === 'stopped' || s.processStatus === 'external' ? 'disabled' : '')+'>Stop</button>'
480+ + '<button class="btn-sm btn-restart" data-service="'+s.name+'" '+(s.processStatus === 'external' ? 'disabled' : '')+' >Restart</button>'
335481 + '</div></td>'
336482 + '<td><a href="http://localhost:'+s.port+'" target="_blank">Open</a></td>'
337483 + '</tr>';
@@ -670,22 +816,23 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
670816 </span>
671817 ${ s . processStatus && s . processStatus !== 'stopped' ? `
672818 <div style="font-size:0.6rem;color:#6b7280;margin-top:2px;">
673- PID: ${ s . pid || 'N/A' } | Uptime: ${ s . uptime ? Math . floor ( s . uptime / 60 ) + 'm' : '0m' }
819+ ${ s . processStatus === 'external' ? 'External Process' : ` PID: ${ s . pid || 'N/A' } | Uptime: ${ s . uptime ? Math . floor ( s . uptime / 60 ) + 'm' : '0m' } ` }
674820 </div>
675821 ` : '' }
676822 </td>
677823 <td><span class="path">${ s . path || `services/${ s . name } ` } </span></td>
678824 <td>
679825 <div class="service-controls">
680826 <button class="btn-sm btn-start" data-service="${ s . name } "
681- ${ s . processStatus === 'running' ? 'disabled' : '' } >
827+ ${ s . processStatus === 'running' || s . processStatus === 'external' ? 'disabled' : '' } >
682828 Start
683829 </button>
684830 <button class="btn-sm btn-stop" data-service="${ s . name } "
685- ${ s . processStatus === 'stopped' ? 'disabled' : '' } >
831+ ${ s . processStatus === 'stopped' || s . processStatus === 'external' ? 'disabled' : '' } >
686832 Stop
687833 </button>
688- <button class="btn-sm btn-restart" data-service="${ s . name } ">
834+ <button class="btn-sm btn-restart" data-service="${ s . name } "
835+ ${ s . processStatus === 'external' ? 'disabled' : '' } >
689836 Restart
690837 </button>
691838 </div>
0 commit comments