This commit is contained in:
jarek committed 2026-03-02 07:59:58 +01:00
1 parent 4b430340db
commit 9c451aedf9
120 files changed
+7777 -6046

No files matched your search

+31 -19
View File
@@ -1,9 +1,10 @@
import { json } from '@sveltejs/kit';
import { listComposeStacks, deployStack, saveStackComposeFile, writeStackEnvFile, writeRawStackEnvFile, saveStackEnvVarsToDb } from '$lib/server/stacks';
import { EnvironmentNotFoundError } from '$lib/server/docker';
import { EnvironmentNotFoundError, DockerConnectionError } from '$lib/server/docker';
import { upsertStackSource, getStackSources } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import { createJobResponse } from '$lib/server/sse';
import type { RequestHandler } from './$types';
export const GET: RequestHandler = async ({ url, cookies }) => {
@@ -62,8 +63,11 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
if (error instanceof EnvironmentNotFoundError) {
return json({ error: 'Environment not found' }, { status: 404 });
}
// Silently return empty for connection errors (offline environments)
if (error instanceof DockerConnectionError) {
return json([]);
}
console.error('Error listing compose stacks:', error);
// Return empty array instead of error to allow UI to load
return json([]);
}
};
@@ -162,20 +166,7 @@ export const POST: RequestHandler = async (event) => {
}
}
// Deploy and start the stack
const result = await deployStack({
name,
compose,
envId: envIdNum,
composePath: composePath || undefined,
envPath: envPath || undefined
});
if (!result.success) {
return json({ error: result.error, output: result.output }, { status: 400 });
}
// Record the stack as internally created with custom paths if provided
// Record the stack in DB before deploying - ensures it exists even if deploy fails
await upsertStackSource({
stackName: name,
environmentId: envIdNum,
@@ -184,10 +175,31 @@ export const POST: RequestHandler = async (event) => {
envPath: envPath || undefined
});
// Audit log (create + deploy in one action)
await auditStack(event, 'deploy', name, envIdNum);
// Deploy via SSE to keep connection alive during long operations
return createJobResponse(async (send) => {
try {
const result = await deployStack({
name,
compose,
envId: envIdNum,
composePath: composePath || undefined,
envPath: envPath || undefined
});
return json({ success: true, started: true, output: result.output });
if (!result.success) {
send('result', { success: false, error: result.error, output: result.output });
return;
}
// Audit log (create + deploy in one action)
await auditStack(event, 'deploy', name, envIdNum);
send('result', { success: true, started: true, output: result.output });
} catch (error: any) {
console.error('Error deploying compose stack:', error);
send('result', { success: false, error: error.message || 'Failed to deploy stack' });
}
}, request);
} catch (error: any) {
console.error('Error creating compose stack:', error);
return json({ error: error.message || 'Failed to create stack' }, { status: 500 });
+27 -12
View File
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
import type { RequestHandler } from './$types';
import { getStackComposeFile, deployStack, saveStackComposeFile } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { createJobResponse } from '$lib/server/sse';
// GET /api/stacks/[name]/compose - Get compose file content
export const GET: RequestHandler = async ({ params, url, cookies }) => {
@@ -66,7 +67,6 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) =>
? { composePath, envPath, moveFromDir, oldComposePath, oldEnvPath }
: undefined;
let result;
if (restart) {
// Deploy with docker compose up -d --force-recreate
// Force recreate ensures env var changes are applied
@@ -79,19 +79,34 @@ export const PUT: RequestHandler = async ({ params, request, url, cookies }) =>
}
// Get authoritative paths from DB/filesystem for deploy
const composeInfo = await getStackComposeFile(name, envIdNum);
result = await deployStack({
name,
compose: content,
envId: envIdNum,
forceRecreate: true,
composePath: composeInfo.composePath || undefined,
envPath: composeInfo.envPath || undefined
});
} else {
// Just save the file without restarting (update operation, not create)
result = await saveStackComposeFile(name, content, false, envIdNum, pathOptions);
// Deploy via SSE to keep connection alive during long operations
return createJobResponse(async (send) => {
try {
const result = await deployStack({
name,
compose: content,
envId: envIdNum,
forceRecreate: true,
composePath: composeInfo.composePath || undefined,
envPath: composeInfo.envPath || undefined
});
if (!result.success) {
send('result', { success: false, error: result.error });
return;
}
send('result', { success: true });
} catch (error: any) {
console.error(`Error deploying stack ${name}:`, error);
send('result', { success: false, error: error.message || 'Failed to deploy stack' });
}
}, request);
}
// Just save the file without restarting (update operation, not create)
const result = await saveStackComposeFile(name, content, false, envIdNum, pathOptions);
if (!result.success) {
return json({ error: result.error }, { status: 500 });
}
+30 -25
View File
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
import { downStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import { createJobResponse } from '$lib/server/sse';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async (event) => {
@@ -21,31 +22,35 @@ export const POST: RequestHandler = async (event) => {
return json({ error: 'Access denied to this environment' }, { status: 403 });
}
// Parse body BEFORE creating SSE response (body can only be read once)
let removeVolumes = false;
try {
// Parse body for optional removeVolumes flag
let removeVolumes = false;
try {
const body = await request.json();
removeVolumes = body.removeVolumes === true;
} catch {
// No body or invalid JSON - use defaults
}
const stackName = decodeURIComponent(params.name);
const result = await downStack(stackName, envIdNum, removeVolumes);
// Audit log
await auditStack(event, 'down', stackName, envIdNum, { removeVolumes });
if (!result.success) {
return json({ success: false, error: result.error }, { status: 400 });
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
console.error('Error downing compose stack:', error);
return json({ error: 'Failed to down compose stack' }, { status: 500 });
const body = await request.json();
removeVolumes = body.removeVolumes === true;
} catch {
// No body or invalid JSON - use defaults
}
return createJobResponse(async (send) => {
try {
const stackName = decodeURIComponent(params.name);
const result = await downStack(stackName, envIdNum, removeVolumes);
// Audit log
await auditStack(event, 'down', stackName, envIdNum, { removeVolumes });
if (!result.success) {
send('result', { success: false, error: result.error });
return;
}
send('result', { success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
send('result', { success: false, error: error.message });
return;
}
console.error('Error downing compose stack:', error);
send('result', { success: false, error: 'Failed to down compose stack' });
}
}, request);
};
+2 -2
View File
@@ -2,7 +2,7 @@ import { json } from '@sveltejs/kit';
import { getStackEnvVars, setStackEnvVars, getStackSource } from '$lib/server/db';
import { findStackDir } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { existsSync } from 'node:fs';
import { existsSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import type { RequestHandler } from './$types';
@@ -94,7 +94,7 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
// Internal/adopted stacks: non-secrets from file, secrets from DB
if (envFilePath && existsSync(envFilePath)) {
try {
const content = await Bun.file(envFilePath).text();
const content = readFileSync(envFilePath, 'utf-8');
const fileVars = parseEnvFile(content);
for (const [key, value] of Object.entries(fileVars)) {
variables.push({ key, value, isSecret: false });
+3 -3
View File
@@ -2,7 +2,7 @@ import { json } from '@sveltejs/kit';
import { findStackDir, getStackDir } from '$lib/server/stacks';
import { getStackSource } from '$lib/server/db';
import { authorize } from '$lib/server/authorize';
import { existsSync, rmSync } from 'node:fs';
import { existsSync, rmSync, readFileSync, writeFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import type { RequestHandler } from './$types';
@@ -58,7 +58,7 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
let content = '';
if (envFilePath && existsSync(envFilePath)) {
try {
content = await Bun.file(envFilePath).text();
content = readFileSync(envFilePath, 'utf-8');
} catch {
// File read failed
}
@@ -154,7 +154,7 @@ export const PUT: RequestHandler = async ({ params, url, cookies, request }) =>
content += '\n';
}
await Bun.write(envFilePath, content);
writeFileSync(envFilePath, content);
return json({ success: true });
} catch (error) {
+20 -15
View File
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
import { restartStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import { createJobResponse } from '$lib/server/sse';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async (event) => {
@@ -21,22 +22,26 @@ export const POST: RequestHandler = async (event) => {
return json({ error: 'Access denied to this environment' }, { status: 403 });
}
try {
const stackName = decodeURIComponent(params.name);
const result = await restartStack(stackName, envIdNum);
return createJobResponse(async (send) => {
try {
const stackName = decodeURIComponent(params.name);
const result = await restartStack(stackName, envIdNum);
// Audit log
await auditStack(event, 'restart', stackName, envIdNum);
// Audit log
await auditStack(event, 'restart', stackName, envIdNum);
if (!result.success) {
return json({ success: false, error: result.error }, { status: 400 });
if (!result.success) {
send('result', { success: false, error: result.error });
return;
}
send('result', { success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
send('result', { success: false, error: error.message });
return;
}
console.error('Error restarting compose stack:', error);
send('result', { success: false, error: 'Failed to restart compose stack' });
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
console.error('Error restarting compose stack:', error);
return json({ error: 'Failed to restart compose stack' }, { status: 500 });
}
}, event.request);
};
+20 -15
View File
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
import { startStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import { createJobResponse } from '$lib/server/sse';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async (event) => {
@@ -21,22 +22,26 @@ export const POST: RequestHandler = async (event) => {
return json({ error: 'Access denied to this environment' }, { status: 403 });
}
try {
const stackName = decodeURIComponent(params.name);
const result = await startStack(stackName, envIdNum);
return createJobResponse(async (send) => {
try {
const stackName = decodeURIComponent(params.name);
const result = await startStack(stackName, envIdNum);
// Audit log
await auditStack(event, 'start', stackName, envIdNum);
// Audit log
await auditStack(event, 'start', stackName, envIdNum);
if (!result.success) {
return json({ success: false, error: result.error }, { status: 400 });
if (!result.success) {
send('result', { success: false, error: result.error });
return;
}
send('result', { success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
send('result', { success: false, error: error.message });
return;
}
console.error('Error starting compose stack:', error);
send('result', { success: false, error: 'Failed to start compose stack' });
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
console.error('Error starting compose stack:', error);
return json({ error: 'Failed to start compose stack' }, { status: 500 });
}
}, event.request);
};
+20 -15
View File
@@ -2,6 +2,7 @@ import { json } from '@sveltejs/kit';
import { stopStack, ComposeFileNotFoundError } from '$lib/server/stacks';
import { authorize } from '$lib/server/authorize';
import { auditStack } from '$lib/server/audit';
import { createJobResponse } from '$lib/server/sse';
import type { RequestHandler } from './$types';
export const POST: RequestHandler = async (event) => {
@@ -21,22 +22,26 @@ export const POST: RequestHandler = async (event) => {
return json({ error: 'Access denied to this environment' }, { status: 403 });
}
try {
const stackName = decodeURIComponent(params.name);
const result = await stopStack(stackName, envIdNum);
return createJobResponse(async (send) => {
try {
const stackName = decodeURIComponent(params.name);
const result = await stopStack(stackName, envIdNum);
// Audit log
await auditStack(event, 'stop', stackName, envIdNum);
// Audit log
await auditStack(event, 'stop', stackName, envIdNum);
if (!result.success) {
return json({ success: false, error: result.error }, { status: 400 });
if (!result.success) {
send('result', { success: false, error: result.error });
return;
}
send('result', { success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
send('result', { success: false, error: error.message });
return;
}
console.error('Error stopping compose stack:', error);
send('result', { success: false, error: 'Failed to stop compose stack' });
}
return json({ success: true, output: result.output });
} catch (error) {
if (error instanceof ComposeFileNotFoundError) {
return json({ error: error.message }, { status: 404 });
}
console.error('Error stopping compose stack:', error);
return json({ error: 'Failed to stop compose stack' }, { status: 500 });
}
}, event.request);
};
+1
View File
@@ -23,6 +23,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
// Detect which stacks are already running on any environment
const discoveredWithRunning = await detectRunningStacks(result.discovered);
discoveredWithRunning.sort((a, b) => a.name.localeCompare(b.name));
return json({
...result,