From 7ba6c4074e267ba963bfea840d5035d82ac18a8f Mon Sep 17 00:00:00 2001 From: Jannis Braun <151788261+TheZwiss@users.noreply.github.com> Date: Mon, 23 Mar 2026 02:24:25 +0100 Subject: [PATCH] feat: add upload limit setting and media retention cleanup to Storage panel --- .../instanceSettingsPanels/StoragePanel.tsx | 133 ++++++++++++++++++ 1 file changed, 133 insertions(+) diff --git a/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx b/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx index 0f3d3006..b8db18e5 100644 --- a/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx +++ b/packages/web/src/components/modals/instanceSettingsPanels/StoragePanel.tsx @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback } from 'react'; import { api } from '../../../api/client'; import type { StorageStats, CleanupResult } from '@backspace/shared'; +import { useSettingsStore } from '../../../stores/settingsStore'; function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; @@ -18,6 +19,19 @@ export function StoragePanel() { const [cleaning, setCleaning] = useState(false); const [previewDone, setPreviewDone] = useState(false); + // Upload limit state + const instanceSettings = useSettingsStore((s) => s.instanceSettings); + const updateInstanceSettings = useSettingsStore((s) => s.updateInstanceSettings); + const [uploadLimitMb, setUploadLimitMb] = useState(100); + const [uploadLimitSaving, setUploadLimitSaving] = useState(false); + const [uploadLimitDirty, setUploadLimitDirty] = useState(false); + + // Media retention state + const [mediaAgeDays, setMediaAgeDays] = useState(90); + const [mediaCleanupResult, setMediaCleanupResult] = useState(null); + const [mediaCleaning, setMediaCleaning] = useState(false); + const [mediaPreviewDone, setMediaPreviewDone] = useState(false); + const fetchStats = useCallback(async () => { setLoading(true); setError(''); @@ -35,6 +49,44 @@ export function StoragePanel() { fetchStats(); }, [fetchStats]); + useEffect(() => { + if (instanceSettings?.maxUploadSizeMb) { + setUploadLimitMb(instanceSettings.maxUploadSizeMb); + } + }, [instanceSettings]); + + const handleUploadLimitSave = async () => { + setUploadLimitSaving(true); + try { + await updateInstanceSettings({ maxUploadSizeMb: uploadLimitMb }); + setUploadLimitDirty(false); + } catch { + setError('Failed to update upload limit'); + } finally { + setUploadLimitSaving(false); + } + }; + + const handleMediaCleanup = async (dryRun: boolean) => { + setMediaCleaning(true); + setMediaCleanupResult(null); + setError(''); + try { + const result = await api.admin.cleanupOldMedia(mediaAgeDays, dryRun); + setMediaCleanupResult(result); + if (dryRun) { + setMediaPreviewDone(true); + } else { + setMediaPreviewDone(false); + await fetchStats(); + } + } catch (err) { + setError(err instanceof Error ? err.message : 'Media cleanup failed'); + } finally { + setMediaCleaning(false); + } + }; + const handleCleanup = async (dryRun: boolean) => { setCleaning(true); setCleanupResult(null); @@ -136,6 +188,32 @@ export function StoragePanel() { )} + {/* Upload Limit */} +
+
Upload Limit
+
+
+ + { setUploadLimitMb(Number(e.target.value)); setUploadLimitDirty(true); }} + className="input-standard w-20 px-2 py-1 text-sm text-center" + /> + MB + +
+
+
+ {/* Cleanup Actions */}
Cleanup
@@ -188,6 +266,61 @@ export function StoragePanel() {
+ {/* Media Retention */} +
+
Media Retention
+
+
+ + { setMediaAgeDays(Number(e.target.value)); setMediaPreviewDone(false); setMediaCleanupResult(null); }} + className="input-standard w-20 px-2 py-1 text-sm text-center" + /> + days +
+ +
+ + +
+ + {mediaCleanupResult && ( +
+
+ {mediaCleanupResult.dryRun ? 'Preview — no files deleted' : 'Cleanup complete'} +
+
+ {mediaCleanupResult.deletedFiles} file{mediaCleanupResult.deletedFiles !== 1 ? 's' : ''} ({formatBytes(mediaCleanupResult.freedBytes)}) +
+ {mediaCleanupResult.errors.length > 0 && ( +
+ {mediaCleanupResult.errors.length} error{mediaCleanupResult.errors.length !== 1 ? 's' : ''}: {mediaCleanupResult.errors[0]} +
+ )} +
+ )} +
+
+ {/* Error / Refresh */} {error && (
{error}