feat(maintenance): add Docker build-cache prune action

Add an admin-only POST /api/docker/prune-build-cache endpoint plus a Settings > Maintenance danger-zone button to reclaim disk used by the Docker build cache (image + static-site builds), which previously grew unbounded with no UI lever. Prunes unused-only (all=false) so a warm cache is preserved for apps redeploying soon. Mirrors the existing prune-images vertical slice; full en/ru i18n parity.
This commit is contained in:
2026-06-02 13:34:05 +03:00
parent 15e5b186cd
commit 97f338fba3
7 changed files with 117 additions and 1 deletions
@@ -6,7 +6,7 @@
never within casual miss-click distance of general form fields.
-->
<script lang="ts">
import { getSettings, updateSettings, pruneImages } from '$lib/api';
import { getSettings, updateSettings, pruneImages, pruneBuildCache } from '$lib/api';
import FormField from '$lib/components/FormField.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
import Skeleton from '$lib/components/Skeleton.svelte';
@@ -18,6 +18,8 @@
let saving = $state(false);
let pruning = $state(false);
let showPruneConfirm = $state(false);
let pruningCache = $state(false);
let showPruneCacheConfirm = $state(false);
let staleThresholdDays = $state('7');
let imagePruneThresholdMb = $state('1024');
@@ -77,6 +79,18 @@
}
}
async function handlePruneBuildCache() {
pruningCache = true;
try {
const result = await pruneBuildCache();
toasts.success($t('settings.pruneCacheResult', { count: String(result.caches_deleted), mb: String(result.space_reclaimed_mb) }));
} catch (err) {
toasts.error(err instanceof Error ? err.message : $t('settings.pruneCacheFailed'));
} finally {
pruningCache = false;
}
}
$effect(() => { load(); });
</script>
@@ -165,6 +179,26 @@
{/if}
</button>
</div>
<hr class="my-5 border-[var(--color-danger)]/20" />
<p class="text-sm text-[var(--text-secondary)]">{$t('settings.buildCacheCleanupHelp')}</p>
<div class="mt-4">
<button
type="button"
onclick={() => { showPruneCacheConfirm = true; }}
disabled={pruningCache}
class="inline-flex items-center gap-2 rounded-lg border border-[var(--color-danger)] bg-[var(--surface-card)] px-4 py-2 text-sm font-medium text-[var(--color-danger)] hover:bg-[var(--color-danger)] hover:text-white disabled:opacity-50 transition-colors"
>
{#if pruningCache}
<IconLoader size={16} />
{$t('settings.pruningCache')}
{:else}
{$t('settings.pruneBuildCache')}
{/if}
</button>
</div>
</div>
</div>
</div>
@@ -180,3 +214,13 @@
onconfirm={() => { showPruneConfirm = false; handlePruneImages(); }}
oncancel={() => { showPruneConfirm = false; }}
/>
<ConfirmDialog
open={showPruneCacheConfirm}
title={$t('settings.pruneBuildCache')}
message={$t('settings.pruneBuildCacheConfirmMessage')}
confirmLabel={$t('settings.pruneBuildCache')}
confirmVariant="danger"
onconfirm={() => { showPruneCacheConfirm = false; handlePruneBuildCache(); }}
oncancel={() => { showPruneCacheConfirm = false; }}
/>