diff --git a/backend/src/index.ts b/backend/src/index.ts index a3051700..b05548a9 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -492,7 +492,7 @@ app.post('/api/stacks/:stackName/deploy', async (req: Request, res: Response) => app.post('/api/stacks/:stackName/down', async (req: Request, res: Response) => { try { const stackName = req.params.stackName as string; - composeService.runCommand(stackName, 'down', terminalWs || undefined); + await composeService.runCommand(stackName, 'down', terminalWs || undefined); res.json({ status: 'Command started' }); } catch (error) { res.status(500).json({ error: 'Failed to start command' }); @@ -502,7 +502,7 @@ app.post('/api/stacks/:stackName/down', async (req: Request, res: Response) => { app.post('/api/stacks/:stackName/restart', async (req: Request, res: Response) => { try { const stackName = req.params.stackName as string; - composeService.runCommand(stackName, 'restart', terminalWs || undefined); + await composeService.runCommand(stackName, 'restart', terminalWs || undefined); res.json({ status: 'Command started' }); } catch (error) { console.error('Failed to restart containers:', error); @@ -513,7 +513,7 @@ app.post('/api/stacks/:stackName/restart', async (req: Request, res: Response) = app.post('/api/stacks/:stackName/stop', async (req: Request, res: Response) => { try { const stackName = req.params.stackName as string; - composeService.runCommand(stackName, 'stop', terminalWs || undefined); + await composeService.runCommand(stackName, 'stop', terminalWs || undefined); res.json({ status: 'Command started' }); } catch (error) { console.error('Failed to stop containers:', error); @@ -524,7 +524,7 @@ app.post('/api/stacks/:stackName/stop', async (req: Request, res: Response) => { app.post('/api/stacks/:stackName/start', async (req: Request, res: Response) => { try { const stackName = req.params.stackName as string; - composeService.runCommand(stackName, 'start', terminalWs || undefined); + await composeService.runCommand(stackName, 'start', terminalWs || undefined); res.json({ status: 'Command started' }); } catch (error) { console.error('Failed to start containers:', error); @@ -536,13 +536,11 @@ app.post('/api/stacks/:stackName/start', async (req: Request, res: Response) => app.post('/api/stacks/:stackName/update', async (req: Request, res: Response) => { try { const stackName = req.params.stackName as string; - // Run update asynchronously, don't wait for completion - composeService.updateStack(stackName, terminalWs || undefined).catch(error => { - console.error('Update stack error:', error); - }); - res.json({ status: 'Update started' }); + // Await update completion + await composeService.updateStack(stackName, terminalWs || undefined); + res.json({ status: 'Update completed' }); } catch (error) { - res.status(500).json({ error: 'Failed to start update' }); + res.status(500).json({ error: 'Failed to update' }); } }); diff --git a/backend/src/services/ComposeService.ts b/backend/src/services/ComposeService.ts index a42d5de0..03b942f6 100644 --- a/backend/src/services/ComposeService.ts +++ b/backend/src/services/ComposeService.ts @@ -16,39 +16,58 @@ export class ComposeService { * CRITICAL: cwd is set to the stack directory so relative paths in compose files * resolve correctly inside the isolated stack folder */ - runCommand(stackName: string, action: 'down' | 'start' | 'stop' | 'restart', ws?: WebSocket) { + async runCommand(stackName: string, action: 'down' | 'start' | 'stop' | 'restart', ws?: WebSocket): Promise { const stackDir = path.join(this.baseDir, stackName); // Run docker compose from within the stack directory // This ensures relative paths (e.g., ./data:/config) resolve correctly const args = ['compose', '-p', stackName, action]; - const child = spawn('docker', args, { - cwd: stackDir, // CRITICAL: Set working directory to stack folder - env: { - ...process.env, - PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + return new Promise((resolve, reject) => { + const child = spawn('docker', args, { + cwd: stackDir, // CRITICAL: Set working directory to stack folder + env: { + ...process.env, + PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + } + }); + + if (ws) { + child.stdout.on('data', (data: Buffer) => { + ws.send(data.toString()); + }); + + child.stderr.on('data', (data: Buffer) => { + ws.send(data.toString()); + }); + + child.on('close', (code: number | null) => { + ws.send(`Command exited with code ${code}\n`); + if (code === 0) resolve(); + else reject(new Error(`Command exited with code ${code}`)); + }); + + child.on('error', (error: Error) => { + console.error(`Docker Compose Error for ${stackName}:`, error.message); + ws.send(`Error: ${error.message}\n`); + reject(error); + }); + } else { + // Without WS, just wait for resolution + let stderr = ''; + child.stderr.on('data', (data: Buffer) => { stderr += data.toString(); }); + + child.on('close', (code: number | null) => { + if (code === 0) resolve(); + else reject(new Error(`Command failed with code ${code}. Stderr: ${stderr}`)); + }); + + child.on('error', (error: Error) => { + console.error(`Docker Compose Error for ${stackName}:`, error.message); + reject(error); + }); } }); - - if (ws) { - child.stdout.on('data', (data: Buffer) => { - ws.send(data.toString()); - }); - - child.stderr.on('data', (data: Buffer) => { - ws.send(data.toString()); - }); - - child.on('close', (code: number | null) => { - ws.send(`Command exited with code ${code}\n`); - }); - - child.on('error', (error: Error) => { - console.error(`Docker Compose Error for ${stackName}:`, error.message); - ws.send(`Error: ${error.message}\n`); - }); - } } /** diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 67aafb57..7a1f6ce8 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -899,7 +899,7 @@ export default function EditorLayout() { - CPU: {containerStats[container?.Id]?.cpu || 'N/A'} | RAM: {containerStats[container?.Id]?.ram || 'N/A'} | NET: {containerStats[container?.Id]?.net || '0 B ↓ / 0 B ↑'} + CPU: {container.State === 'running' ? (containerStats[container?.Id]?.cpu || 'N/A') : '0.00%'} | RAM: {container.State === 'running' ? (containerStats[container?.Id]?.ram || 'N/A') : '0.00 MB'} | NET: {container.State === 'running' ? (containerStats[container?.Id]?.net || '0 B ↓ / 0 B ↑') : '0 B/s ↓ / 0 B/s ↑'}