v1.0.21
This commit is contained in:
1 parent
790ce092ee
commit
55f3101a19
51 files changed
+763
-599
No files matched your search
@@ -30,8 +30,11 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
const details = await inspectContainer(params.id, envIdNum);
|
||||
return json(details);
|
||||
} catch (error) {
|
||||
console.error('Error inspecting container:', error);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error inspecting container:', error?.message || error);
|
||||
return json({ error: 'Failed to inspect container' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -96,8 +99,11 @@ export const DELETE: RequestHandler = async (event) => {
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error removing container:', error);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error removing container:', error?.message || error);
|
||||
return json({ error: 'Failed to remove container' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -26,7 +26,10 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
return json(result);
|
||||
} catch (error: any) {
|
||||
console.error('Error listing container directory:', error);
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error listing container directory:', error?.message || error);
|
||||
return json({ error: error.message || 'Failed to list directory' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -36,7 +36,10 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
return json({ content, path });
|
||||
} catch (error: any) {
|
||||
console.error('Error reading container file:', error);
|
||||
if (error?.statusCode === 404 && !error.message?.includes('No such file')) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error reading container file:', error?.message || error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('No such file or directory')) {
|
||||
@@ -92,7 +95,7 @@ export const PUT: RequestHandler = async ({ params, url, cookies, request }) =>
|
||||
|
||||
return json({ success: true, path });
|
||||
} catch (error: any) {
|
||||
console.error('Error writing container file:', error);
|
||||
console.error('Error writing container file:', error?.message || error);
|
||||
const msg = error.message || String(error);
|
||||
|
||||
if (msg.includes('Permission denied')) {
|
||||
|
||||
@@ -76,7 +76,7 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
return new Response(body, { headers });
|
||||
} catch (error: any) {
|
||||
console.error('Error downloading container file:', error);
|
||||
console.error('Error downloading container file:', error?.message || error);
|
||||
|
||||
if (error.message?.includes('No such file or directory')) {
|
||||
return new Response(JSON.stringify({ error: 'File not found' }), {
|
||||
|
||||
@@ -140,7 +140,7 @@ export const POST: RequestHandler = async ({ params, url, request, cookies }) =>
|
||||
errors: errors.length > 0 ? errors : undefined
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error uploading to container:', error);
|
||||
console.error('Error uploading to container:', error?.message || error);
|
||||
|
||||
if (error.message?.includes('Permission denied')) {
|
||||
return json({ error: 'Permission denied to write to this path' }, { status: 403 });
|
||||
|
||||
@@ -46,8 +46,11 @@ export const POST: RequestHandler = async (event) => {
|
||||
}
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error renaming container:', error);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error renaming container:', error?.message || error);
|
||||
return json({ error: 'Failed to rename container' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -38,8 +38,11 @@ export const POST: RequestHandler = async (event) => {
|
||||
await auditContainer(event, 'restart', params.id, containerName, envIdNum);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error restarting container:', error);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error restarting container:', error?.message || error);
|
||||
return json({ error: 'Failed to restart container' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -31,8 +31,11 @@ export const POST: RequestHandler = async (event) => {
|
||||
await auditContainer(event, 'start', params.id, containerName, envIdNum);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error starting container:', error);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error starting container:', error?.message || error);
|
||||
return json({ error: 'Failed to start container' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -31,8 +31,11 @@ export const POST: RequestHandler = async (event) => {
|
||||
await auditContainer(event, 'stop', params.id, containerName, envIdNum);
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error) {
|
||||
console.error('Error stopping container:', error);
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error stopping container:', error?.message || error);
|
||||
return json({ error: 'Failed to stop container' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -47,8 +47,11 @@ export const POST: RequestHandler = async (event) => {
|
||||
await auditContainer(event, 'update', container.id, options.name, envIdNum, { ...options, startAfterUpdate });
|
||||
|
||||
return json({ success: true, id: container.id });
|
||||
} catch (error) {
|
||||
console.error('Error updating container:', error);
|
||||
return json({ error: 'Failed to update container', details: String(error) }, { status: 500 });
|
||||
} catch (error: any) {
|
||||
if (error?.statusCode === 404) {
|
||||
return json({ error: error.json?.message || 'Container not found' }, { status: 404 });
|
||||
}
|
||||
console.error('Error updating container:', error?.message || error);
|
||||
return json({ error: 'Failed to update container', details: error?.message || String(error) }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { authorize } from '$lib/server/authorize';
|
||||
import { listContainers, inspectContainer, checkImageUpdateAvailable } from '$lib/server/docker';
|
||||
import { clearPendingContainerUpdates, addPendingContainerUpdate } from '$lib/server/db';
|
||||
import { isSystemContainer } from '$lib/server/scheduler/tasks/update-utils';
|
||||
import { createJobResponse } from '$lib/server/sse';
|
||||
|
||||
export interface UpdateCheckResult {
|
||||
containerId: string;
|
||||
@@ -19,9 +20,9 @@ export interface UpdateCheckResult {
|
||||
|
||||
/**
|
||||
* Check all containers for available image updates.
|
||||
* Returns all results at once after checking in parallel.
|
||||
* Returns progress events during checking, final result when done.
|
||||
*/
|
||||
export const POST: RequestHandler = async ({ url, cookies }) => {
|
||||
export const POST: RequestHandler = async ({ url, cookies, request }) => {
|
||||
const auth = await authorize(cookies);
|
||||
|
||||
const envId = url.searchParams.get('env');
|
||||
@@ -32,21 +33,21 @@ export const POST: RequestHandler = async ({ url, cookies }) => {
|
||||
return json({ error: 'Permission denied' }, { status: 403 });
|
||||
}
|
||||
|
||||
try {
|
||||
return createJobResponse(async (send) => {
|
||||
// Clear existing pending updates for this environment before checking
|
||||
if (envIdNum) {
|
||||
await clearPendingContainerUpdates(envIdNum);
|
||||
}
|
||||
|
||||
const allContainers = await listContainers(true, envIdNum);
|
||||
|
||||
// Include all containers (system containers get flagged, not filtered)
|
||||
const containers = allContainers;
|
||||
|
||||
send('progress', { checked: 0, total: containers.length });
|
||||
|
||||
// Check container for updates
|
||||
let checked = 0;
|
||||
const checkContainer = async (container: typeof containers[0]): Promise<UpdateCheckResult> => {
|
||||
try {
|
||||
// Get container's image name from config
|
||||
const inspectData = await inspectContainer(container.id, envIdNum) as any;
|
||||
const imageName = inspectData.Config?.Image;
|
||||
const currentImageId = inspectData.Image;
|
||||
@@ -62,7 +63,6 @@ export const POST: RequestHandler = async ({ url, cookies }) => {
|
||||
};
|
||||
}
|
||||
|
||||
// Use shared update detection function
|
||||
const result = await checkImageUpdateAvailable(imageName, currentImageId, envIdNum);
|
||||
|
||||
return {
|
||||
@@ -88,13 +88,23 @@ export const POST: RequestHandler = async ({ url, cookies }) => {
|
||||
}
|
||||
};
|
||||
|
||||
// Check all containers in parallel
|
||||
const results = await Promise.all(containers.map(checkContainer));
|
||||
// Sliding window concurrency limit to avoid DNS threadpool saturation (#676).
|
||||
const CONCURRENCY = 20;
|
||||
const results: UpdateCheckResult[] = new Array(containers.length);
|
||||
let next = 0;
|
||||
async function runNext(): Promise<void> {
|
||||
while (next < containers.length) {
|
||||
const idx = next++;
|
||||
results[idx] = await checkContainer(containers[idx]);
|
||||
checked++;
|
||||
send('progress', { checked, total: containers.length });
|
||||
}
|
||||
}
|
||||
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, containers.length) }, () => runNext()));
|
||||
|
||||
const updatesFound = results.filter(r => r.hasUpdate).length;
|
||||
|
||||
// Save containers with updates to the database for persistence
|
||||
// Skip system containers (Dockhand/Hawser) - they use their own update paths
|
||||
if (envIdNum) {
|
||||
for (const result of results) {
|
||||
if (result.hasUpdate && !result.systemContainer) {
|
||||
@@ -108,13 +118,10 @@ export const POST: RequestHandler = async ({ url, cookies }) => {
|
||||
}
|
||||
}
|
||||
|
||||
return json({
|
||||
send('result', {
|
||||
total: containers.length,
|
||||
updatesFound,
|
||||
results
|
||||
});
|
||||
} catch (error: any) {
|
||||
console.error('Error checking for updates:', error);
|
||||
return json({ error: 'Failed to check for updates', details: error.message }, { status: 500 });
|
||||
}
|
||||
}, request);
|
||||
};
|
||||
@@ -24,6 +24,7 @@ import { authorize } from '$lib/server/authorize';
|
||||
import { prefersJSON, sseToJSON } from '$lib/server/sse';
|
||||
import type { EnvironmentStats } from '../+server';
|
||||
import { parseLabels } from '$lib/utils/label-colors';
|
||||
import { isEdgeConnected } from '$lib/server/hawser';
|
||||
|
||||
|
||||
// Skip disk usage collection (Synology NAS performance fix)
|
||||
@@ -249,6 +250,22 @@ async function getEnvironmentStatsProgressive(
|
||||
loading: { ...envStats.loading }
|
||||
});
|
||||
|
||||
// For edge envs with no connected agent, skip the 5s ping and fail immediately.
|
||||
// On restart, agents take 30-70s to reconnect — without this check, every open
|
||||
// dashboard tab fires a 5s ping per edge env simultaneously, creating a flood.
|
||||
if (env.connectionType === 'hawser-edge' && !isEdgeConnected(env.id)) {
|
||||
envStats.online = false;
|
||||
envStats.error = 'Agent not connected';
|
||||
envStats.loading = undefined;
|
||||
onPartialUpdate({
|
||||
id: env.id,
|
||||
online: false,
|
||||
error: 'Agent not connected',
|
||||
loading: undefined
|
||||
});
|
||||
return envStats;
|
||||
}
|
||||
|
||||
// Quick reachability check — if ping fails, skip all expensive Docker API calls
|
||||
if (!await dockerPing(env.id)) {
|
||||
envStats.online = false;
|
||||
|
||||
@@ -80,8 +80,14 @@ export const POST: RequestHandler = async (event) => {
|
||||
return json({ error: 'An environment with this name already exists' }, { status: 409 });
|
||||
}
|
||||
|
||||
// Host is required for direct and hawser-standard connections
|
||||
// Validate connection type
|
||||
const validConnectionTypes = ['socket', 'direct', 'hawser-standard', 'hawser-edge'];
|
||||
const connectionType = data.connectionType || 'socket';
|
||||
if (!validConnectionTypes.includes(connectionType)) {
|
||||
return json({ error: `Invalid connection type: ${connectionType}` }, { status: 400 });
|
||||
}
|
||||
|
||||
// Host is required for direct and hawser-standard connections
|
||||
if ((connectionType === 'direct' || connectionType === 'hawser-standard') && !data.host) {
|
||||
return json({ error: 'Host is required for this connection type' }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -152,7 +152,7 @@ export const GET: RequestHandler = async ({ url, cookies }) => {
|
||||
|
||||
return json(hostInfo);
|
||||
} catch (error) {
|
||||
console.error('Failed to get host info:', error);
|
||||
console.error('Failed to get host info:', (error as Error)?.message ?? error);
|
||||
return json({ error: 'Failed to get host info' }, { status: 500 });
|
||||
}
|
||||
};
|
||||
@@ -49,7 +49,9 @@ async function fetchDockerHubTags(imageName: string, page: number = 1, pageSize:
|
||||
if (response.status === 404) {
|
||||
throw new Error('Image not found on Docker Hub');
|
||||
}
|
||||
throw new Error(`Docker Hub returned error: ${response.status}`);
|
||||
const err = new Error(`Docker Hub returned error: ${response.status}`) as any;
|
||||
err.statusCode = response.status;
|
||||
throw err;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
@@ -162,6 +164,9 @@ export const GET: RequestHandler = async ({ url }) => {
|
||||
if (error.code === 'ENOTFOUND') {
|
||||
return json({ error: 'Registry host not found' }, { status: 503 });
|
||||
}
|
||||
if (error.statusCode) {
|
||||
return json({ error: error.message || 'Failed to fetch tags' }, { status: error.statusCode });
|
||||
}
|
||||
|
||||
return json({ error: error.message || 'Failed to fetch tags' }, { status: 500 });
|
||||
}
|
||||
|
||||
@@ -1,26 +1,41 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getOwnContainerId, getHostDockerSocket } from '$lib/server/host-path';
|
||||
import { getOwnContainerId, getHostDockerSocket, getOwnDockerHost, getOwnNetworkMode } from '$lib/server/host-path';
|
||||
import { buildRegistryAuthHeader, unixSocketRequest, unixSocketStreamRequest } from '$lib/server/docker';
|
||||
import type { RequestHandler } from './$types';
|
||||
import { prefersJSON, sseToJSON } from '$lib/server/sse';
|
||||
|
||||
const UPDATER_IMAGE = 'fnsys/dockhand-updater:latest';
|
||||
const UPDATER_LABEL = 'dockhand.updater';
|
||||
const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||
|
||||
/** Fetch from the local Docker socket (buffered). */
|
||||
function localDockerFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
return unixSocketRequest(DOCKER_SOCKET, path, options);
|
||||
/** Get TCP Docker host if configured, null otherwise. */
|
||||
function getDockerTcpHost(): string | null {
|
||||
const dockerHost = process.env.DOCKER_HOST || getOwnDockerHost();
|
||||
return dockerHost?.startsWith('tcp://') ? dockerHost : null;
|
||||
}
|
||||
|
||||
/** Fetch from the local Docker socket (streaming body for pull progress). */
|
||||
/** Fetch from the local Docker (buffered). Supports TCP and Unix socket. */
|
||||
function localDockerFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
const tcpHost = getDockerTcpHost();
|
||||
if (tcpHost) {
|
||||
return fetch(tcpHost.replace('tcp://', 'http://') + path, options);
|
||||
}
|
||||
const socketPath = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||
return unixSocketRequest(socketPath, path, options);
|
||||
}
|
||||
|
||||
/** Fetch from the local Docker (streaming body for pull progress). */
|
||||
function localDockerStreamFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
return unixSocketStreamRequest(DOCKER_SOCKET, path, options);
|
||||
const tcpHost = getDockerTcpHost();
|
||||
if (tcpHost) {
|
||||
return fetch(tcpHost.replace('tcp://', 'http://') + path, options);
|
||||
}
|
||||
const socketPath = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||
return unixSocketStreamRequest(socketPath, path, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull an image via local Docker socket, streaming progress via callback.
|
||||
* Pull an image via local Docker, streaming progress via callback.
|
||||
*/
|
||||
async function pullImageLocal(imageName: string, onProgress?: (line: string) => void): Promise<void> {
|
||||
let fromImage = imageName;
|
||||
@@ -79,9 +94,14 @@ async function pullImageLocal(imageName: string, onProgress?: (line: string) =>
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Docker socket is mounted read-write
|
||||
* Check if Docker access allows write operations.
|
||||
* TCP connections always allow writes (no RO mount concept).
|
||||
* Socket connections check if the mount is read-write.
|
||||
*/
|
||||
async function isDockerSocketWritable(containerId: string): Promise<boolean> {
|
||||
async function isDockerWritable(containerId: string): Promise<boolean> {
|
||||
// TCP connections don't have mount-level RO/RW — access implies full control
|
||||
if (getDockerTcpHost()) return true;
|
||||
|
||||
const response = await localDockerFetch(`/containers/${containerId}/json`);
|
||||
if (!response.ok) return false;
|
||||
|
||||
@@ -235,7 +255,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
return json({ error: 'Not running in Docker' }, { status: 400 });
|
||||
}
|
||||
|
||||
const writable = await isDockerSocketWritable(containerId);
|
||||
const writable = await isDockerWritable(containerId);
|
||||
if (!writable) {
|
||||
return json({
|
||||
error: 'Docker socket is mounted read-only. Self-update requires read-write Docker socket access.'
|
||||
@@ -252,8 +272,6 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
return json({ error: 'Failed to determine container name' }, { status: 500 });
|
||||
}
|
||||
|
||||
const socketHostPath = getHostDockerSocket();
|
||||
|
||||
// Start SSE stream for preparation progress
|
||||
const encoder = new TextEncoder();
|
||||
let controllerClosed = false;
|
||||
@@ -353,6 +371,25 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
...networkEnvVars
|
||||
];
|
||||
|
||||
// Configure updater's Docker access based on connection type
|
||||
const tcpHost = getDockerTcpHost();
|
||||
const updaterHostConfig: Record<string, unknown> = { AutoRemove: true };
|
||||
|
||||
if (tcpHost) {
|
||||
// TCP: pass DOCKER_HOST so docker CLI in sidecar uses TCP
|
||||
updaterEnv.push(`DOCKER_HOST=${tcpHost}`);
|
||||
// Put sidecar on same network so it can reach the Docker TCP endpoint
|
||||
const network = getOwnNetworkMode();
|
||||
if (network) {
|
||||
updaterHostConfig.NetworkMode = network;
|
||||
}
|
||||
send('log', { message: `Updater using TCP: ${tcpHost}` });
|
||||
} else {
|
||||
// Socket: bind-mount the host Docker socket
|
||||
const socketHostPath = getHostDockerSocket();
|
||||
updaterHostConfig.Binds = [`${socketHostPath}:/var/run/docker.sock`];
|
||||
}
|
||||
|
||||
console.log('[SelfUpdate] Creating updater container...');
|
||||
const updaterResponse = await localDockerFetch('/containers/create?name=dockhand-updater', {
|
||||
method: 'POST',
|
||||
@@ -363,12 +400,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
Labels: {
|
||||
[UPDATER_LABEL]: 'true'
|
||||
},
|
||||
HostConfig: {
|
||||
AutoRemove: true,
|
||||
Binds: [
|
||||
`${socketHostPath}:/var/run/docker.sock`
|
||||
]
|
||||
}
|
||||
HostConfig: updaterHostConfig
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -1,15 +1,23 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getOwnContainerId } from '$lib/server/host-path';
|
||||
import { getOwnContainerId, getOwnDockerHost } from '$lib/server/host-path';
|
||||
import { getRegistryManifestDigest, unixSocketRequest } from '$lib/server/docker';
|
||||
import { compareVersions } from '$lib/utils/version';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||
|
||||
/** Fetch from the local Docker socket directly (not through environment routing) */
|
||||
/** Fetch from the local Docker directly (not through environment routing) */
|
||||
function localDockerFetch(path: string, options: RequestInit = {}): Promise<Response> {
|
||||
return unixSocketRequest(DOCKER_SOCKET, path, options);
|
||||
const dockerHost = process.env.DOCKER_HOST || getOwnDockerHost();
|
||||
|
||||
if (dockerHost?.startsWith('tcp://')) {
|
||||
// TCP connection (socat proxy, socket-proxy, remote Docker)
|
||||
const url = dockerHost.replace('tcp://', 'http://') + path;
|
||||
return fetch(url, options);
|
||||
}
|
||||
|
||||
// Unix socket (default)
|
||||
const socketPath = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||
return unixSocketRequest(socketPath, path, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
import { json } from '@sveltejs/kit';
|
||||
import { authorize } from '$lib/server/authorize';
|
||||
import { getOwnDockerHost } from '$lib/server/host-path';
|
||||
import { unixSocketRequest } from '$lib/server/docker';
|
||||
import type { RequestHandler } from './$types';
|
||||
|
||||
const DOCKER_SOCKET = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||
|
||||
/** Fetch from the local Docker socket directly */
|
||||
/** Fetch from the local Docker directly. Supports TCP and Unix socket. */
|
||||
function localDockerFetch(path: string): Promise<Response> {
|
||||
return unixSocketRequest(DOCKER_SOCKET, path);
|
||||
const dockerHost = process.env.DOCKER_HOST || getOwnDockerHost();
|
||||
if (dockerHost?.startsWith('tcp://')) {
|
||||
return fetch(dockerHost.replace('tcp://', 'http://') + path);
|
||||
}
|
||||
const socketPath = process.env.DOCKER_SOCKET || '/var/run/docker.sock';
|
||||
return unixSocketRequest(socketPath, path);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -65,6 +65,8 @@ export interface GeneralSettings {
|
||||
gridFontSize: string;
|
||||
terminalFont: string;
|
||||
editorFont: string;
|
||||
// Compact ports
|
||||
compactPorts: boolean;
|
||||
// External stack paths
|
||||
externalStackPaths: string[];
|
||||
// Primary stack location
|
||||
@@ -85,6 +87,7 @@ const DEFAULT_SETTINGS: Omit<GeneralSettings, 'scheduleRetentionDays' | 'eventRe
|
||||
eventCollectionMode: 'stream',
|
||||
eventPollInterval: 60000,
|
||||
metricsCollectionInterval: 30000,
|
||||
compactPorts: false,
|
||||
lightTheme: 'default',
|
||||
darkTheme: 'default',
|
||||
font: 'system',
|
||||
@@ -140,6 +143,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
gridFontSize,
|
||||
terminalFont,
|
||||
editorFont,
|
||||
compactPorts,
|
||||
externalStackPaths,
|
||||
primaryStackLocation
|
||||
] = await Promise.all([
|
||||
@@ -169,6 +173,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
getSetting('theme_grid_font_size'),
|
||||
getSetting('theme_terminal_font'),
|
||||
getSetting('theme_editor_font'),
|
||||
getSetting('compact_ports'),
|
||||
getExternalStackPaths(),
|
||||
getPrimaryStackLocation()
|
||||
]);
|
||||
@@ -200,6 +205,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
|
||||
gridFontSize: gridFontSize ?? DEFAULT_SETTINGS.gridFontSize,
|
||||
terminalFont: terminalFont ?? DEFAULT_SETTINGS.terminalFont,
|
||||
editorFont: editorFont ?? DEFAULT_SETTINGS.editorFont,
|
||||
compactPorts: compactPorts ?? DEFAULT_SETTINGS.compactPorts,
|
||||
externalStackPaths,
|
||||
primaryStackLocation
|
||||
};
|
||||
@@ -219,7 +225,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
|
||||
try {
|
||||
const body = await request.json();
|
||||
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, externalStackPaths, primaryStackLocation } = body;
|
||||
const { confirmDestructive, showStoppedContainers, highlightUpdates, timeFormat, dateFormat, downloadFormat, defaultGrypeArgs, defaultTrivyArgs, scheduleRetentionDays, eventRetentionDays, scheduleCleanupCron, eventCleanupCron, scheduleCleanupEnabled, eventCleanupEnabled, logBufferSizeKb, defaultTimezone, eventCollectionMode, eventPollInterval, metricsCollectionInterval, lightTheme, darkTheme, font, fontSize, gridFontSize, terminalFont, editorFont, compactPorts, externalStackPaths, primaryStackLocation } = body;
|
||||
|
||||
if (confirmDestructive !== undefined) {
|
||||
await setSetting('confirm_destructive', confirmDestructive);
|
||||
@@ -312,6 +318,9 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
if (editorFont !== undefined && VALID_EDITOR_FONTS.includes(editorFont)) {
|
||||
await setSetting('theme_editor_font', editorFont);
|
||||
}
|
||||
if (compactPorts !== undefined) {
|
||||
await setSetting('compact_ports', compactPorts);
|
||||
}
|
||||
if (externalStackPaths !== undefined && Array.isArray(externalStackPaths)) {
|
||||
// Filter to valid non-empty strings
|
||||
const validPaths = externalStackPaths.filter((p: unknown) => typeof p === 'string' && p.trim());
|
||||
@@ -355,6 +364,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
gridFontSizeVal,
|
||||
terminalFontVal,
|
||||
editorFontVal,
|
||||
compactPortsVal,
|
||||
externalStackPathsVal,
|
||||
primaryStackLocationVal
|
||||
] = await Promise.all([
|
||||
@@ -384,6 +394,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
getSetting('theme_grid_font_size'),
|
||||
getSetting('theme_terminal_font'),
|
||||
getSetting('theme_editor_font'),
|
||||
getSetting('compact_ports'),
|
||||
getExternalStackPaths(),
|
||||
getPrimaryStackLocation()
|
||||
]);
|
||||
@@ -415,6 +426,7 @@ export const POST: RequestHandler = async ({ request, cookies }) => {
|
||||
gridFontSize: gridFontSizeVal ?? DEFAULT_SETTINGS.gridFontSize,
|
||||
terminalFont: terminalFontVal ?? DEFAULT_SETTINGS.terminalFont,
|
||||
editorFont: editorFontVal ?? DEFAULT_SETTINGS.editorFont,
|
||||
compactPorts: compactPortsVal ?? DEFAULT_SETTINGS.compactPorts,
|
||||
externalStackPaths: externalStackPathsVal,
|
||||
primaryStackLocation: primaryStackLocationVal
|
||||
};
|
||||
|
||||
@@ -24,9 +24,10 @@ export const GET: RequestHandler = async ({ params, url, cookies }) => {
|
||||
|
||||
const volume = await inspectVolume(params.name, envIdNum);
|
||||
return json(volume);
|
||||
} catch (error) {
|
||||
console.error('Failed to inspect volume:', error);
|
||||
return json({ error: 'Failed to inspect volume' }, { status: 500 });
|
||||
} catch (error: any) {
|
||||
const status = error.statusCode ?? 500;
|
||||
console.error(`Failed to inspect volume ${params.name}: ${error.message}`);
|
||||
return json({ error: 'Failed to inspect volume' }, { status });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -57,7 +58,12 @@ export const DELETE: RequestHandler = async (event) => {
|
||||
|
||||
return json({ success: true });
|
||||
} catch (error: any) {
|
||||
console.error('Failed to remove volume:', error);
|
||||
return json({ error: 'Failed to remove volume', details: error.message }, { status: 500 });
|
||||
const status = error.statusCode ?? 500;
|
||||
if (status === 404) {
|
||||
console.warn(`Failed to remove volume ${params.name}: ${error.message}`);
|
||||
} else {
|
||||
console.error(`Failed to remove volume ${params.name}: ${error.message}`);
|
||||
}
|
||||
return json({ error: 'Failed to remove volume', details: error.message }, { status });
|
||||
}
|
||||
};
|
||||
Reference in new issue
Block a user