diff --git a/src/lib/server/subprocesses/metrics-subprocess.ts b/src/lib/server/subprocesses/metrics-subprocess.ts index c331ae5..6b6dffd 100644 --- a/src/lib/server/subprocesses/metrics-subprocess.ts +++ b/src/lib/server/subprocesses/metrics-subprocess.ts @@ -247,6 +247,12 @@ async function checkEnvDiskSpace(env: { id: number; name: string; collectMetrics return; } + // Check if disk warnings are enabled for this environment + const diskWarningEnabled = (await getEnvSetting('disk_warning_enabled', env.id)) ?? true; + if (!diskWarningEnabled) { + return; + } + // Check if we're in cooldown for this environment const lastWarningTime = lastDiskWarning.get(env.id); if (lastWarningTime && Date.now() - lastWarningTime < DISK_WARNING_COOLDOWN) { @@ -292,41 +298,48 @@ async function checkEnvDiskSpace(env: { id: number; name: string; collectMetrics } } - // If we found total disk space, calculate percentage - if (dataSpaceTotal > 0) { - diskPercentUsed = (totalUsed / dataSpaceTotal) * 100; - } else { - // Fallback: just report absolute usage if we can't determine percentage - const GB = 1024 * 1024 * 1024; - if (totalUsed > 50 * GB) { + // Determine warning mode + const diskWarningMode = (await getEnvSetting('disk_warning_mode', env.id)) ?? 'percentage'; + const GB = 1024 * 1024 * 1024; + + if (diskWarningMode === 'absolute') { + // Absolute mode: warn when usage exceeds GB threshold + const thresholdGb = (await getEnvSetting('disk_warning_threshold_gb', env.id)) ?? 50; + if (totalUsed > thresholdGb * GB) { send({ type: 'disk_warning', envId: env.id, envName: env.name, - message: `Environment "${env.name}" is using ${formatSize(totalUsed)} of Docker disk space` + message: `Environment "${env.name}" is using ${formatSize(totalUsed)} of Docker disk space (threshold: ${thresholdGb} GB)` }); lastDiskWarning.set(env.id, Date.now()); } - return; - } + } else { + // Percentage mode: need total disk space + if (dataSpaceTotal > 0) { + diskPercentUsed = (totalUsed / dataSpaceTotal) * 100; + } else { + // Can't determine percentage without total space — skip + return; + } - // Check against threshold - const threshold = - (await getEnvSetting('disk_warning_threshold', env.id)) || DEFAULT_DISK_THRESHOLD; - if (diskPercentUsed >= threshold) { - console.log( - `[MetricsSubprocess] Docker disk usage for ${env.name}: ${diskPercentUsed.toFixed(1)}% (threshold: ${threshold}%)` - ); + const threshold = + (await getEnvSetting('disk_warning_threshold', env.id)) || DEFAULT_DISK_THRESHOLD; + if (diskPercentUsed >= threshold) { + console.log( + `[MetricsSubprocess] Docker disk usage for ${env.name}: ${diskPercentUsed.toFixed(1)}% (threshold: ${threshold}%)` + ); - send({ - type: 'disk_warning', - envId: env.id, - envName: env.name, - message: `Environment "${env.name}" Docker disk usage is at ${diskPercentUsed.toFixed(1)}% (${formatSize(totalUsed)} used)`, - diskPercent: diskPercentUsed - }); + send({ + type: 'disk_warning', + envId: env.id, + envName: env.name, + message: `Environment "${env.name}" Docker disk usage is at ${diskPercentUsed.toFixed(1)}% (${formatSize(totalUsed)} used)`, + diskPercent: diskPercentUsed + }); - lastDiskWarning.set(env.id, Date.now()); + lastDiskWarning.set(env.id, Date.now()); + } } } catch (error) { // Skip this environment if it fails diff --git a/src/routes/api/auth/settings/+server.ts b/src/routes/api/auth/settings/+server.ts index 8e4ecc9..c38c31d 100644 --- a/src/routes/api/auth/settings/+server.ts +++ b/src/routes/api/auth/settings/+server.ts @@ -62,6 +62,11 @@ export const PUT: RequestHandler = async ({ request, cookies }) => { } } + // Enforce minimum session timeout of 1 hour + if (data.sessionTimeout !== undefined) { + data.sessionTimeout = Math.max(3600, Math.min(604800, parseInt(data.sessionTimeout) || 86400)); + } + const settings = await updateAuthSettings(data); return json(settings); } catch (error) { diff --git a/src/routes/api/environments/[id]/disk-warning/+server.ts b/src/routes/api/environments/[id]/disk-warning/+server.ts new file mode 100644 index 0000000..45b465c --- /dev/null +++ b/src/routes/api/environments/[id]/disk-warning/+server.ts @@ -0,0 +1,64 @@ +import { json } from '@sveltejs/kit'; +import type { RequestHandler } from './$types'; +import { authorize } from '$lib/server/authorize'; +import { getEnvironment, getEnvSetting, setEnvSetting } from '$lib/server/db'; + +export const GET: RequestHandler = async ({ params, cookies }) => { + const auth = await authorize(cookies); + if (auth.authEnabled && !(await auth.can('environments', 'view'))) { + return json({ error: 'Permission denied' }, { status: 403 }); + } + + try { + const id = parseInt(params.id); + const env = await getEnvironment(id); + if (!env) { + return json({ error: 'Environment not found' }, { status: 404 }); + } + + const enabled = (await getEnvSetting('disk_warning_enabled', id)) ?? true; + const mode = (await getEnvSetting('disk_warning_mode', id)) ?? 'percentage'; + const threshold = (await getEnvSetting('disk_warning_threshold', id)) ?? 80; + const thresholdGb = (await getEnvSetting('disk_warning_threshold_gb', id)) ?? 50; + + return json({ enabled, mode, threshold, thresholdGb }); + } catch (error) { + console.error('Failed to get disk warning settings:', error); + return json({ error: 'Failed to get disk warning settings' }, { status: 500 }); + } +}; + +export const POST: RequestHandler = async ({ params, request, cookies }) => { + const auth = await authorize(cookies); + if (auth.authEnabled && !(await auth.can('environments', 'edit'))) { + return json({ error: 'Permission denied' }, { status: 403 }); + } + + try { + const id = parseInt(params.id); + const env = await getEnvironment(id); + if (!env) { + return json({ error: 'Environment not found' }, { status: 404 }); + } + + const data = await request.json(); + + if (typeof data.enabled === 'boolean') { + await setEnvSetting('disk_warning_enabled', data.enabled, id); + } + if (data.mode === 'percentage' || data.mode === 'absolute') { + await setEnvSetting('disk_warning_mode', data.mode, id); + } + if (typeof data.threshold === 'number' && data.threshold >= 1 && data.threshold <= 100) { + await setEnvSetting('disk_warning_threshold', data.threshold, id); + } + if (typeof data.thresholdGb === 'number' && data.thresholdGb >= 1) { + await setEnvSetting('disk_warning_threshold_gb', data.thresholdGb, id); + } + + return json({ success: true }); + } catch (error) { + console.error('Failed to save disk warning settings:', error); + return json({ error: 'Failed to save disk warning settings' }, { status: 500 }); + } +}; diff --git a/src/routes/settings/auth/AuthTab.svelte b/src/routes/settings/auth/AuthTab.svelte index 90cdc7e..21f889f 100644 --- a/src/routes/settings/auth/AuthTab.svelte +++ b/src/routes/settings/auth/AuthTab.svelte @@ -264,7 +264,11 @@ value={sessionTimeout} min={3600} max={604800} - onchange={(e) => (sessionTimeout = parseInt(e.currentTarget.value))} + onchange={(e) => { + const val = parseInt(e.currentTarget.value); + sessionTimeout = Math.max(3600, Math.min(604800, isNaN(val) ? 86400 : val)); + e.currentTarget.value = String(sessionTimeout); + }} class="w-32" disabled={!$canAccess('settings', 'edit')} /> diff --git a/src/routes/settings/environments/EnvironmentModal.svelte b/src/routes/settings/environments/EnvironmentModal.svelte index 4cda99b..3b274df 100644 --- a/src/routes/settings/environments/EnvironmentModal.svelte +++ b/src/routes/settings/environments/EnvironmentModal.svelte @@ -262,6 +262,10 @@ let formCollectActivity = $state(true); let formCollectMetrics = $state(true); let formHighlightChanges = $state(true); + let formDiskWarningEnabled = $state(true); + let formDiskWarningMode = $state<'percentage' | 'absolute'>('percentage'); + let formDiskWarningThreshold = $state(80); + let formDiskWarningThresholdGb = $state(50); let formConnectionType = $state('socket'); let formHawserToken = $state(''); let formLabels = $state([]); @@ -445,6 +449,7 @@ loadUpdateCheckSettings(environment.id); loadImagePruneSettings(environment.id); loadTimezone(environment.id); + loadDiskWarningSettings(environment.id); // Load Hawser token if edge mode if (formConnectionType === 'hawser-edge') { loadHawserToken(environment.id); @@ -463,6 +468,10 @@ formCollectActivity = true; formCollectMetrics = true; formHighlightChanges = true; + formDiskWarningEnabled = true; + formDiskWarningMode = 'percentage'; + formDiskWarningThreshold = 80; + formDiskWarningThresholdGb = 50; formConnectionType = 'socket'; formHawserToken = ''; formLabels = []; @@ -701,6 +710,7 @@ // Save timezone if not default if (newEnv?.id) { await saveTimezone(newEnv.id); + await saveDiskWarningSettings(newEnv.id); } onSaved(); onClose(); @@ -774,6 +784,7 @@ await saveUpdateCheckSettings(environment.id); await saveImagePruneSettings(environment.id); await saveTimezone(environment.id); + await saveDiskWarningSettings(environment.id); toast.success(`Updated environment: ${formName}`); onSaved(); onClose(); @@ -788,6 +799,39 @@ } } + // === Disk Warning Functions === + async function loadDiskWarningSettings(envId: number) { + try { + const response = await fetch(`/api/environments/${envId}/disk-warning`); + if (response.ok) { + const data = await response.json(); + formDiskWarningEnabled = data.enabled ?? true; + formDiskWarningMode = data.mode ?? 'percentage'; + formDiskWarningThreshold = data.threshold ?? 80; + formDiskWarningThresholdGb = data.thresholdGb ?? 50; + } + } catch (error) { + console.error('Failed to load disk warning settings:', error); + } + } + + async function saveDiskWarningSettings(envId: number) { + try { + await fetch(`/api/environments/${envId}/disk-warning`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + enabled: formDiskWarningEnabled, + mode: formDiskWarningMode, + threshold: formDiskWarningThreshold, + thresholdGb: formDiskWarningThresholdGb + }) + }); + } catch (error) { + console.error('Failed to save disk warning settings:', error); + } + } + // === Timezone Functions === async function loadTimezone(envId: number) { try { @@ -2063,6 +2107,10 @@ bind:collectActivity={formCollectActivity} bind:collectMetrics={formCollectMetrics} bind:highlightChanges={formHighlightChanges} + bind:diskWarningEnabled={formDiskWarningEnabled} + bind:diskWarningMode={formDiskWarningMode} + bind:diskWarningThreshold={formDiskWarningThreshold} + bind:diskWarningThresholdGb={formDiskWarningThresholdGb} /> diff --git a/src/routes/settings/environments/tabs/ActivityTab.svelte b/src/routes/settings/environments/tabs/ActivityTab.svelte index 1f509a1..b3026e7 100644 --- a/src/routes/settings/environments/tabs/ActivityTab.svelte +++ b/src/routes/settings/environments/tabs/ActivityTab.svelte @@ -1,17 +1,28 @@ @@ -36,3 +47,64 @@ + +
+
+
+ +

Send notifications when Docker disk usage exceeds the threshold

+
+ +
+ + {#if diskWarningEnabled} +
+ { if (v) diskWarningMode = v as 'percentage' | 'absolute'; }}> + +
+ {#if diskWarningMode === 'percentage'} + + Percentage + {:else} + + Absolute (GB) + {/if} +
+
+ + +
+ + Percentage +
+
+ +
+ + Absolute (GB) +
+
+
+
+ + {#if diskWarningMode === 'percentage'} + + % + {:else} + + GB + {/if} +
+ {/if} +