diff --git a/backend/src/index.ts b/backend/src/index.ts index 8592e500..38d4f11b 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -11,6 +11,7 @@ import { ConfigService } from './services/ConfigService'; import composerize from 'composerize'; import si from 'systeminformation'; import http from 'http'; +import { spawn } from 'child_process'; const app = express(); const PORT = 3000; @@ -215,11 +216,54 @@ server.on('upgrade', async (req, socket, head) => { const logsMatch = url.match(/^\/api\/stacks\/([^/]+)\/logs$/); if (logsMatch) { - // Dedicated stack logs WebSocket + // Dedicated stack logs WebSocket - uses raw Docker CLI to support legacy containers const logsWss = new WebSocket.Server({ noServer: true }); - logsWss.handleUpgrade(req, socket, head, (ws) => { + logsWss.handleUpgrade(req, socket, head, async (ws) => { const stackName = decodeURIComponent(logsMatch[1]); - composeService.streamLogs(stackName, ws); + try { + const dockerController = DockerController.getInstance(); + const containers = await dockerController.getContainersByStack(stackName); + if (!containers || containers.length === 0) { + ws.send('No containers running to log.\n'); + return; + } + // Stream logs from all containers using raw docker logs CLI + const childProcesses: ReturnType[] = []; + for (const container of containers) { + const containerName = container.Names?.[0]?.replace(/^\//, '') || container.Id; + const logProcess = spawn('docker', ['logs', '-f', '--tail', '100', containerName], { + env: { + ...process.env, + PATH: process.env.PATH || '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin' + } + }); + childProcesses.push(logProcess); + logProcess.stdout.on('data', (data: Buffer) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data.toString()); + } + }); + logProcess.stderr.on('data', (data: Buffer) => { + if (ws.readyState === WebSocket.OPEN) { + ws.send(data.toString()); + } + }); + logProcess.on('error', (error: Error) => { + console.error(`Docker logs error for ${containerName}:`, error.message); + }); + } + // Kill all log processes when WebSocket closes + ws.on('close', () => { + for (const proc of childProcesses) { + try { proc.kill(); } catch { /* ignore */ } + } + }); + } catch (error) { + console.error('Failed to stream logs:', error); + if (ws.readyState === WebSocket.OPEN) { + ws.send(`Error streaming logs: ${(error as Error).message}\n`); + } + } }); } else { // Generic terminal WebSocket @@ -434,6 +478,42 @@ app.post('/api/stacks/:stackName/down', async (req: Request, res: Response) => { } }); +// Direct container restart - bypasses docker compose for legacy container support +app.post('/api/stacks/:stackName/restart', async (req: Request, res: Response) => { + try { + const stackName = req.params.stackName as string; + const dockerController = DockerController.getInstance(); + const containers = await dockerController.getContainersByStack(stackName); + if (containers && containers.length > 0) { + for (const c of containers) { + await dockerController.restartContainer(c.Id); + } + } + res.json({ status: 'Containers restarted' }); + } catch (error) { + console.error('Failed to restart containers:', error); + res.status(500).json({ error: 'Failed to restart containers' }); + } +}); + +// Direct container stop - bypasses docker compose for legacy container support +app.post('/api/stacks/:stackName/stop', async (req: Request, res: Response) => { + try { + const stackName = req.params.stackName as string; + const dockerController = DockerController.getInstance(); + const containers = await dockerController.getContainersByStack(stackName); + if (containers && containers.length > 0) { + for (const c of containers) { + await dockerController.stopContainer(c.Id); + } + } + res.json({ status: 'Containers stopped' }); + } catch (error) { + console.error('Failed to stop containers:', error); + res.status(500).json({ error: 'Failed to stop containers' }); + } +}); + // Update stack: pull images and recreate containers app.post('/api/stacks/:stackName/update', async (req: Request, res: Response) => { try { diff --git a/frontend/src/components/EditorLayout.tsx b/frontend/src/components/EditorLayout.tsx index 0d84d80b..11cc5551 100644 --- a/frontend/src/components/EditorLayout.tsx +++ b/frontend/src/components/EditorLayout.tsx @@ -247,7 +247,7 @@ export default function EditorLayout() { if (!selectedFile) return; const stackName = selectedFile.replace(/\.(yml|yaml)$/, ''); try { - await apiFetch(`/stacks/${stackName}/down`, { + await apiFetch(`/stacks/${stackName}/stop`, { method: 'POST', }); // Refresh containers after stop @@ -265,10 +265,7 @@ export default function EditorLayout() { if (!selectedFile) return; const stackName = selectedFile.replace(/\.(yml|yaml)$/, ''); try { - await apiFetch(`/stacks/${stackName}/down`, { - method: 'POST', - }); - await apiFetch(`/stacks/${stackName}/up`, { + await apiFetch(`/stacks/${stackName}/restart`, { method: 'POST', }); // Refresh containers after restart @@ -490,7 +487,7 @@ export default function EditorLayout() { {getDisplayName(file)}