feat: Initialize core backend server with authentication, WebSocket, and Docker integration, and add initial frontend editor layout.

This commit is contained in:
SaelixCode
2026-02-21 15:58:53 -05:00
parent 18c51f083e
commit 280090bd2b
2 changed files with 86 additions and 9 deletions
+83 -3
View File
@@ -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<typeof spawn>[] = [];
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 {
+3 -6
View File
@@ -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() {
<span className="flex items-center gap-2">
<span
className={`w-2 h-2 rounded-full ${stackStatuses[file] === 'running' ? 'bg-green-500' :
stackStatuses[file] === 'exited' ? 'bg-red-500' : 'bg-gray-400'
stackStatuses[file] === 'exited' ? 'bg-red-500' : 'bg-gray-400'
}`}
/>
{getDisplayName(file)}