Skip to content

Commit 5d69f14

Browse files
meenu155kaifcoder
authored andcommitted
feat(service-controls): implement service start/stop/restart functionality
- Add service control endpoints (start, stop, restart) to admin dashboard - Implement ServiceManager class for process lifecycle management - Add comprehensive test suite for service controls - Update admin UI with service control buttons and status indicators
1 parent f87b921 commit 5d69f14

File tree

2 files changed

+400
-21
lines changed

2 files changed

+400
-21
lines changed

bin/lib/admin.js

Lines changed: 168 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)