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

@@ -3,6 +3,7 @@ import {
getEnvironments,
getLatestHostMetrics,
getHostMetrics,
getMetricsCollectionInterval,
getContainerEventStats,
getContainerEvents,
getEnvSetting,
@@ -14,13 +15,17 @@ import {
listImages,
listNetworks,
getContainerStats,
getDiskUsage
getDiskUsage,
dockerPing,
DockerConnectionError
} from '$lib/server/docker';
import { listComposeStacks } from '$lib/server/stacks';
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';
// Skip disk usage collection (Synology NAS performance fix)
const SKIP_DF_COLLECTION = process.env.SKIP_DF_COLLECTION === 'true' || process.env.SKIP_DF_COLLECTION === '1';
@@ -68,6 +73,9 @@ setInterval(() => {
}
}, 10 * 60 * 1000); // Every 10 minutes
// Register cache reporter for memory monitoring
async function getCachedDiskUsage(envId: number): Promise<any> {
const cached = diskUsageCache.get(envId);
const now = Date.now();
@@ -125,10 +133,14 @@ function calculateMemoryUsage(memoryStats: any): number {
return usage;
}
// Target time window for metrics history charts (15 minutes)
const METRICS_HISTORY_WINDOW_MS = 15 * 60 * 1000;
// Progressive stats loading - returns stats object and emits partial updates via callback
async function getEnvironmentStatsProgressive(
env: any,
onPartialUpdate: (stats: Partial<EnvironmentStats> & { id: number }) => void
onPartialUpdate: (stats: Partial<EnvironmentStats> & { id: number }) => void,
metricsPointCount: number
): Promise<EnvironmentStats> {
const envStats: EnvironmentStats = {
id: env.id,
@@ -187,7 +199,7 @@ async function getEnvironmentStatsProgressive(
getLatestHostMetrics(env.id),
getContainerEventStats(env.id),
getContainerEvents({ environmentId: env.id, limit: 10 }),
getHostMetrics(30, env.id),
getHostMetrics(metricsPointCount, env.id),
getPendingContainerUpdates(env.id)
]);
@@ -237,6 +249,20 @@ async function getEnvironmentStatsProgressive(
loading: { ...envStats.loading }
});
// Quick reachability check — if ping fails, skip all expensive Docker API calls
if (!await dockerPing(env.id)) {
envStats.online = false;
envStats.error = 'Environment offline';
envStats.loading = undefined;
onPartialUpdate({
id: env.id,
online: false,
error: 'Environment offline',
loading: undefined
});
return envStats;
}
// Helper to get valid size
const getValidSize = (size: number | undefined | null): number => {
return size && size > 0 ? size : 0;
@@ -511,7 +537,7 @@ async function getEnvironmentStatsProgressive(
return envStats;
}
export const GET: RequestHandler = async ({ cookies }) => {
export const GET: RequestHandler = async ({ request, cookies }) => {
const auth = await authorize(cookies);
if (auth.authEnabled && !await auth.can('environments', 'view')) {
return new Response(JSON.stringify({ error: 'Permission denied' }), {
@@ -535,6 +561,7 @@ export const GET: RequestHandler = async ({ cookies }) => {
let controllerClosed = false;
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
// Safe enqueue that checks if controller is still open
@@ -573,17 +600,23 @@ export const GET: RequestHandler = async ({ cookies }) => {
}));
safeEnqueue(`event: environments\ndata: ${JSON.stringify(envList)}\n\n`);
// Calculate metrics point count based on configured interval
const metricsIntervalMs = await getMetricsCollectionInterval();
const metricsPointCount = Math.ceil(METRICS_HISTORY_WINDOW_MS / metricsIntervalMs);
// Fetch stats for each environment with progressive updates
const promises = environments.map(async (env) => {
try {
await getEnvironmentStatsProgressive(env, (partialStats) => {
// Send partial update as it arrives
safeEnqueue(`event: partial\ndata: ${JSON.stringify(partialStats)}\n\n`);
});
}, metricsPointCount);
// Send final complete stats event for this environment
safeEnqueue(`event: complete\ndata: ${JSON.stringify({ id: env.id })}\n\n`);
} catch (error) {
console.error(`Failed to get stats for ${env.name}:`, error);
if (!(error instanceof DockerConnectionError)) {
console.error(`Failed to get stats for ${env.name}:`, error);
}
// Convert technical error to user-friendly message
const errorStr = String(error);
let friendlyError = 'Connection error';
@@ -611,19 +644,25 @@ export const GET: RequestHandler = async ({ cookies }) => {
} catch {
// Already closed
}
}
},
cancel() {
// Called when the client disconnects
controllerClosed = true;
}
});
return new Response(stream, {
const sseResponse = new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive'
'Connection': 'keep-alive',
'X-Accel-Buffering': 'no'
}
});
if (prefersJSON(request)) return sseToJSON(sseResponse);
return sseResponse;
};