mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-07-26 11:49:16 +00:00
feat: Initialize core backend server with authentication, WebSocket, and Docker integration, and add initial frontend editor layout.
This commit is contained in:
+83
-3
@@ -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 {
|
||||
|
||||
@@ -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)}
|
||||
|
||||
Reference in New Issue
Block a user