mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-28 19:27:41 +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 composerize from 'composerize';
|
||||||
import si from 'systeminformation';
|
import si from 'systeminformation';
|
||||||
import http from 'http';
|
import http from 'http';
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
const PORT = 3000;
|
const PORT = 3000;
|
||||||
@@ -215,11 +216,54 @@ server.on('upgrade', async (req, socket, head) => {
|
|||||||
const logsMatch = url.match(/^\/api\/stacks\/([^/]+)\/logs$/);
|
const logsMatch = url.match(/^\/api\/stacks\/([^/]+)\/logs$/);
|
||||||
|
|
||||||
if (logsMatch) {
|
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 });
|
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]);
|
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 {
|
} else {
|
||||||
// Generic terminal WebSocket
|
// 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
|
// Update stack: pull images and recreate containers
|
||||||
app.post('/api/stacks/:stackName/update', async (req: Request, res: Response) => {
|
app.post('/api/stacks/:stackName/update', async (req: Request, res: Response) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -247,7 +247,7 @@ export default function EditorLayout() {
|
|||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
const stackName = selectedFile.replace(/\.(yml|yaml)$/, '');
|
const stackName = selectedFile.replace(/\.(yml|yaml)$/, '');
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/stacks/${stackName}/down`, {
|
await apiFetch(`/stacks/${stackName}/stop`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
// Refresh containers after stop
|
// Refresh containers after stop
|
||||||
@@ -265,10 +265,7 @@ export default function EditorLayout() {
|
|||||||
if (!selectedFile) return;
|
if (!selectedFile) return;
|
||||||
const stackName = selectedFile.replace(/\.(yml|yaml)$/, '');
|
const stackName = selectedFile.replace(/\.(yml|yaml)$/, '');
|
||||||
try {
|
try {
|
||||||
await apiFetch(`/stacks/${stackName}/down`, {
|
await apiFetch(`/stacks/${stackName}/restart`, {
|
||||||
method: 'POST',
|
|
||||||
});
|
|
||||||
await apiFetch(`/stacks/${stackName}/up`, {
|
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
});
|
});
|
||||||
// Refresh containers after restart
|
// Refresh containers after restart
|
||||||
@@ -490,7 +487,7 @@ export default function EditorLayout() {
|
|||||||
<span className="flex items-center gap-2">
|
<span className="flex items-center gap-2">
|
||||||
<span
|
<span
|
||||||
className={`w-2 h-2 rounded-full ${stackStatuses[file] === 'running' ? 'bg-green-500' :
|
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)}
|
{getDisplayName(file)}
|
||||||
|
|||||||
Reference in New Issue
Block a user