@@ -5,6 +5,7 @@ import http from 'http';
55import { spawn } from 'node:child_process' ;
66import { WebSocketServer , WebSocket } from 'ws' ;
77import { getLogsForAPI , LogFileWatcher } from './logs.js' ;
8+ import { startService , stopService , restartService , getServiceStatus , getAllServiceStatuses , validateServiceCanRun } from './service-manager.js' ;
89
910// ws helper
1011function sendWebSocketMessage ( ws , message ) {
@@ -98,12 +99,28 @@ async function checkServiceStatus(service) {
9899// Check status of all services
99100export async function getServicesStatus ( services ) {
100101 const statusPromises = services . map ( async ( service ) => {
101- const status = await checkServiceStatus ( service ) ;
102- return {
102+ const healthStatus = await checkServiceStatus ( service ) ;
103+ const processStatus = getServiceStatus ( service . name ) ;
104+
105+ // Combine health check and process management status
106+ const combinedStatus = {
103107 ...service ,
104- ...status ,
108+ ...healthStatus ,
109+ processStatus : processStatus . status ,
110+ pid : processStatus . pid ,
111+ uptime : processStatus . uptime ,
112+ processStartTime : processStatus . startTime ,
105113 lastChecked : new Date ( ) . toISOString ( )
106114 } ;
115+
116+ // Override status if we know the process is managed locally
117+ if ( processStatus . status === 'running' && healthStatus . status === 'down' ) {
118+ combinedStatus . status = 'starting' ; // Process running but not responding yet
119+ } else if ( processStatus . status === 'stopped' && healthStatus . status === 'up' ) {
120+ combinedStatus . status = 'external' ; // Running externally (not managed by us)
121+ }
122+
123+ return combinedStatus ;
107124 } ) ;
108125
109126 return Promise . all ( statusPromises ) ;
@@ -182,11 +199,89 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
182199 .log-level { font-weight:600; margin-right:8px; }
183200 .logs-empty { text-align:center; color:#6b7280; padding:60px 20px; }
184201
202+ /* Service control buttons */
203+ .service-controls { display:flex; gap:4px; flex-wrap:wrap; }
204+ .btn-sm { padding:3px 8px; font-size:0.7rem; border:none; border-radius:3px; cursor:pointer; color:#fff; transition:opacity 0.2s; }
205+ .btn-sm:disabled { opacity:0.5; cursor:not-allowed; }
206+ .btn-start { background:#10b981; }
207+ .btn-start:hover:not(:disabled) { background:#059669; }
208+ .btn-stop { background:#ef4444; }
209+ .btn-stop:hover:not(:disabled) { background:#dc2626; }
210+ .btn-restart { background:#f59e0b; }
211+ .btn-restart:hover:not(:disabled) { background:#d97706; }
212+
213+ /* Additional status styles */
214+ .status-badge.starting { background:#f59e0b; color:#fff; }
215+ .status-badge.external { background:#8b5cf6; color:#fff; }
216+ .dot.starting { background:#f59e0b; }
217+ .dot.external { background:#8b5cf6; }
218+
185219 @media (max-width: 920px) { .layout { flex-direction:column; } .sidebar { width:100%; flex-direction:row; flex-wrap:wrap; } .service-list { display:flex; flex-wrap:wrap; gap:8px; } .service-list li { margin:0; } }
186220 </style>
187221 <script>
188- const REFRESH_MS = ${ refreshInterval } ;
189- let nextRefreshLabel;
222+ var REFRESH_MS = ${ refreshInterval } ;
223+ var nextRefreshLabel;
224+
225+ function startService(serviceName) {
226+ fetch('/api/services/start', {
227+ method: 'POST',
228+ headers: { 'Content-Type': 'application/json' },
229+ body: JSON.stringify({ serviceName: serviceName })
230+ })
231+ .then(function(response) { return response.json(); })
232+ .then(function(result) {
233+ if (result.error) {
234+ alert('Failed to start ' + serviceName + ': ' + result.error);
235+ } else {
236+ alert('Started ' + serviceName + ' successfully');
237+ setTimeout(fetchStatus, 1000);
238+ }
239+ })
240+ .catch(function(error) {
241+ alert('Error starting ' + serviceName + ': ' + error.message);
242+ });
243+ }
244+
245+ function stopService(serviceName) {
246+ fetch('/api/services/stop', {
247+ method: 'POST',
248+ headers: { 'Content-Type': 'application/json' },
249+ body: JSON.stringify({ serviceName: serviceName })
250+ })
251+ .then(function(response) { return response.json(); })
252+ .then(function(result) {
253+ if (result.error) {
254+ alert('Failed to stop ' + serviceName + ': ' + result.error);
255+ } else {
256+ alert('Stopped ' + serviceName + ' successfully');
257+ setTimeout(fetchStatus, 1000);
258+ }
259+ })
260+ .catch(function(error) {
261+ alert('Error stopping ' + serviceName + ': ' + error.message);
262+ });
263+ }
264+
265+ function restartService(serviceName) {
266+ fetch('/api/services/restart', {
267+ method: 'POST',
268+ headers: { 'Content-Type': 'application/json' },
269+ body: JSON.stringify({ serviceName: serviceName })
270+ })
271+ .then(function(response) { return response.json(); })
272+ .then(function(result) {
273+ if (result.error) {
274+ alert('Failed to restart ' + serviceName + ': ' + result.error);
275+ } else {
276+ alert('Restarted ' + serviceName + ' successfully');
277+ setTimeout(fetchStatus, 1000);
278+ }
279+ })
280+ .catch(function(error) {
281+ alert('Error restarting ' + serviceName + ': ' + error.message);
282+ });
283+ }
284+
190285 function scheduleCountdown() {
191286 const el = document.querySelector('.refresh');
192287 if (!el) return;
@@ -223,17 +318,24 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
223318 if (meta) meta.textContent = 'Auto-refresh: ' + (REFRESH_MS/1000).toFixed(1) + 's | Services: ' + services.length;
224319
225320 // Build rows HTML
226- const rows = services.map(s => (
227- '<tr id="row-'+s.name+'">'
321+ const rows = services.map(s => {
322+ 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>' : '';
324+
325+ return '<tr id="row-'+s.name+'">'
228326 + '<td><strong>'+s.name+'</strong></td>'
229327 + '<td>'+s.type+'</td>'
230328 + '<td>'+s.port+'</td>'
231- + '<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></td>'
329+ + '<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>'
232330 + '<td><span class="path">'+(s.path || 'services/'+s.name)+'</span></td>'
233- + '<td>'+new Date(s.lastChecked).toLocaleTimeString()+'</td>'
331+ + '<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>'
335+ + '</div></td>'
234336 + '<td><a href="http://localhost:'+s.port+'" target="_blank">Open</a></td>'
235- + '</tr>'
236- ) ).join('');
337+ + '</tr>';
338+ } ).join('');
237339 tbody.innerHTML = rows;
238340
239341 // Update sidebar dots
@@ -264,6 +366,22 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
264366 scheduleCountdown();
265367 setTimeout(fetchStatus, REFRESH_MS); // initial schedule
266368 initializeLogs();
369+
370+ // Event delegation for service control buttons
371+ document.addEventListener('click', function(event) {
372+ var target = event.target;
373+ var serviceName = target.getAttribute('data-service');
374+
375+ if (!serviceName || target.disabled) return;
376+
377+ if (target.classList.contains('btn-start')) {
378+ startService(serviceName);
379+ } else if (target.classList.contains('btn-stop')) {
380+ stopService(serviceName);
381+ } else if (target.classList.contains('btn-restart')) {
382+ restartService(serviceName);
383+ }
384+ });
267385 });
268386
269387 // Logs functionality (auto-stream)
@@ -470,6 +588,33 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
470588 }
471589 }, delay);
472590 }
591+
592+ function showServiceAction(message, type) {
593+ // Create or update action message element
594+ let actionMsg = document.querySelector('#service-action-msg');
595+ if (!actionMsg) {
596+ actionMsg = document.createElement('div');
597+ actionMsg.id = 'service-action-msg';
598+ actionMsg.style.cssText = 'position:fixed;top:70px;right:20px;padding:10px 15px;border-radius:4px;font-weight:500;z-index:1000;transition:opacity 0.3s;';
599+ document.body.appendChild(actionMsg);
600+ }
601+
602+ // Set message and styling
603+ actionMsg.textContent = message;
604+ actionMsg.style.backgroundColor = type === 'success' ? '#10b981' : '#ef4444';
605+ actionMsg.style.color = 'white';
606+ actionMsg.style.opacity = '1';
607+
608+ // Auto-hide after 3 seconds
609+ setTimeout(() => {
610+ actionMsg.style.opacity = '0';
611+ setTimeout(() => {
612+ if (actionMsg.parentNode) {
613+ actionMsg.parentNode.removeChild(actionMsg);
614+ }
615+ }, 300);
616+ }, 3000);
617+ }
473618 </script>
474619</head>
475620<body>
@@ -508,7 +653,7 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
508653 <th>Port</th>
509654 <th>Status</th>
510655 <th>Path</th>
511- <th>Last Checked </th>
656+ <th>Controls </th>
512657 <th>Link</th>
513658 </tr>
514659 </thead>
@@ -518,9 +663,33 @@ function generateDashboardHTML(servicesWithStatus, refreshInterval = 5000) {
518663 <td><strong>${ s . name } </strong></td>
519664 <td>${ s . type } </td>
520665 <td>${ s . port } </td>
521- <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></td>
666+ <td>
667+ <span class="status-badge ${ s . status } ">
668+ <span class="dot ${ s . status } " style="box-shadow:none;width:8px;height:8px;"></span>
669+ ${ s . status . toUpperCase ( ) }
670+ </span>
671+ ${ s . processStatus && s . processStatus !== 'stopped' ? `
672+ <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' }
674+ </div>
675+ ` : '' }
676+ </td>
522677 <td><span class="path">${ s . path || `services/${ s . name } ` } </span></td>
523- <td>${ new Date ( s . lastChecked ) . toLocaleTimeString ( ) } </td>
678+ <td>
679+ <div class="service-controls">
680+ <button class="btn-sm btn-start" data-service="${ s . name } "
681+ ${ s . processStatus === 'running' ? 'disabled' : '' } >
682+ Start
683+ </button>
684+ <button class="btn-sm btn-stop" data-service="${ s . name } "
685+ ${ s . processStatus === 'stopped' ? 'disabled' : '' } >
686+ Stop
687+ </button>
688+ <button class="btn-sm btn-restart" data-service="${ s . name } ">
689+ Restart
690+ </button>
691+ </div>
692+ </td>
524693 <td><a href="http://localhost:${ s . port } " target="_blank">Open</a></td>
525694 </tr>` ) . join ( '' ) }
526695 </tbody>
@@ -646,6 +815,100 @@ export async function startAdminDashboard(options = {}) {
646815 return ;
647816 }
648817
818+ // Service management endpoints
819+ if ( url . pathname === '/api/services/start' && req . method === 'POST' ) {
820+ try {
821+ let body = '' ;
822+ req . on ( 'data' , chunk => body += chunk ) ;
823+ req . on ( 'end' , async ( ) => {
824+ const { serviceName } = JSON . parse ( body ) ;
825+ const service = cfg . services . find ( s => s . name === serviceName ) ;
826+
827+ if ( ! service ) {
828+ res . writeHead ( 404 , { 'Content-Type' : 'application/json' } ) ;
829+ res . end ( JSON . stringify ( { error : 'Service not found' } ) ) ;
830+ return ;
831+ }
832+
833+ try {
834+ const result = await startService ( service ) ;
835+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
836+ res . end ( JSON . stringify ( result ) ) ;
837+ } catch ( error ) {
838+ res . writeHead ( 500 , { 'Content-Type' : 'application/json' } ) ;
839+ res . end ( JSON . stringify ( { error : error . message } ) ) ;
840+ }
841+ } ) ;
842+ } catch ( error ) {
843+ res . writeHead ( 500 , { 'Content-Type' : 'application/json' } ) ;
844+ res . end ( JSON . stringify ( { error : 'Failed to parse request' } ) ) ;
845+ }
846+ return ;
847+ }
848+
849+ if ( url . pathname === '/api/services/stop' && req . method === 'POST' ) {
850+ try {
851+ let body = '' ;
852+ req . on ( 'data' , chunk => body += chunk ) ;
853+ req . on ( 'end' , async ( ) => {
854+ const { serviceName } = JSON . parse ( body ) ;
855+
856+ try {
857+ const result = await stopService ( serviceName ) ;
858+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
859+ res . end ( JSON . stringify ( result ) ) ;
860+ } catch ( error ) {
861+ res . writeHead ( 500 , { 'Content-Type' : 'application/json' } ) ;
862+ res . end ( JSON . stringify ( { error : error . message } ) ) ;
863+ }
864+ } ) ;
865+ } catch ( error ) {
866+ res . writeHead ( 500 , { 'Content-Type' : 'application/json' } ) ;
867+ res . end ( JSON . stringify ( { error : 'Failed to parse request' } ) ) ;
868+ }
869+ return ;
870+ }
871+
872+ if ( url . pathname === '/api/services/restart' && req . method === 'POST' ) {
873+ try {
874+ let body = '' ;
875+ req . on ( 'data' , chunk => body += chunk ) ;
876+ req . on ( 'end' , async ( ) => {
877+ const { serviceName } = JSON . parse ( body ) ;
878+ const service = cfg . services . find ( s => s . name === serviceName ) ;
879+
880+ if ( ! service ) {
881+ res . writeHead ( 404 , { 'Content-Type' : 'application/json' } ) ;
882+ res . end ( JSON . stringify ( { error : 'Service not found' } ) ) ;
883+ return ;
884+ }
885+
886+ try {
887+ const result = await restartService ( service ) ;
888+ res . writeHead ( 200 , { 'Content-Type' : 'application/json' } ) ;
889+ res . end ( JSON . stringify ( result ) ) ;
890+ } catch ( error ) {
891+ res . writeHead ( 500 , { 'Content-Type' : 'application/json' } ) ;
892+ res . end ( JSON . stringify ( { error : error . message } ) ) ;
893+ }
894+ } ) ;
895+ } catch ( error ) {
896+ res . writeHead ( 500 , { 'Content-Type' : 'application/json' } ) ;
897+ res . end ( JSON . stringify ( { error : 'Failed to parse request' } ) ) ;
898+ }
899+ return ;
900+ }
901+
902+ if ( url . pathname === '/api/services/status' ) {
903+ const statuses = getAllServiceStatuses ( ) ;
904+ res . writeHead ( 200 , {
905+ 'Content-Type' : 'application/json' ,
906+ 'Access-Control-Allow-Origin' : '*'
907+ } ) ;
908+ res . end ( JSON . stringify ( statuses , null , 2 ) ) ;
909+ return ;
910+ }
911+
649912 // Serve dashboard HTML
650913 const servicesWithStatus = await getServicesStatus ( cfg . services ) ;
651914 const html = generateDashboardHTML ( servicesWithStatus , refreshInterval ) ;
0 commit comments