import { useState, useEffect, useCallback } from 'react'; import { api } from '../../../api/client'; import type { StorageStats, CleanupResult } from '@backspace/shared'; function formatBytes(bytes: number): string { if (bytes === 0) return '0 B'; const units = ['B', 'KB', 'MB', 'GB']; const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1); const value = bytes / Math.pow(1024, i); return `${value < 10 ? value.toFixed(2) : value < 100 ? value.toFixed(1) : Math.round(value)} ${units[i]}`; } export function StoragePanel() { const [stats, setStats] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(''); const [cleanupResult, setCleanupResult] = useState(null); const [cleaning, setCleaning] = useState(false); const [previewDone, setPreviewDone] = useState(false); const fetchStats = useCallback(async () => { setLoading(true); setError(''); try { const data = await api.admin.storageStats(); setStats(data); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to load storage stats'); } finally { setLoading(false); } }, []); useEffect(() => { fetchStats(); }, [fetchStats]); const handleCleanup = async (dryRun: boolean) => { setCleaning(true); setCleanupResult(null); setError(''); try { const result = await api.admin.storageCleanup(dryRun); setCleanupResult(result); if (dryRun) { setPreviewDone(true); } else { setPreviewDone(false); await fetchStats(); } } catch (err) { setError(err instanceof Error ? err.message : 'Cleanup failed'); } finally { setCleaning(false); } }; if (loading) { return
Loading storage stats...
; } if (error && !stats) { return (
{error}
); } if (!stats) return null; const hasOrphans = stats.orphanedFiles > 0 || stats.unlinkedAttachments > 0 || stats.danglingAttachments > 0; return (

Storage

Monitor disk usage and clean up orphaned files left behind by deleted content or replaced avatars/banners.
{/* Storage Overview */}
Storage Overview
Total Files
{stats.totalFiles}
{formatBytes(stats.totalSize)}
Referenced
{stats.referencedFiles}
{formatBytes(stats.referencedSize)}
Orphaned Files
0 ? 'text-accent-amber' : 'text-txt-primary'}`}> {stats.orphanedFiles}
{formatBytes(stats.orphanedSize)}
Unlinked Uploads
0 ? 'text-accent-amber' : 'text-txt-primary'}`}> {stats.unlinkedAttachments}
{formatBytes(stats.unlinkedSize)}
Dangling Records
0 ? 'text-accent-amber' : 'text-txt-primary'}`}> {stats.danglingAttachments}
{formatBytes(stats.danglingSize)}
{/* File Type Breakdown */} {stats.breakdown.length > 0 && (
File Type Breakdown
{stats.breakdown.map((b) => (
{b.type} {b.count} file{b.count !== 1 ? 's' : ''} — {formatBytes(b.size)}
))}
)} {/* Cleanup Actions */}
Cleanup
{!hasOrphans && (
No orphaned files, stale uploads, or dangling records found.
)} {hasOrphans && (
)} {cleanupResult && (
{cleanupResult.dryRun ? 'Preview — no files deleted' : 'Cleanup complete'}
{cleanupResult.deletedFiles} orphaned/dangling file{cleanupResult.deletedFiles !== 1 ? 's' : ''} ({formatBytes(cleanupResult.freedBytes)}) {cleanupResult.deletedAttachmentRecords > 0 && ( <>, {cleanupResult.deletedAttachmentRecords} stale upload record{cleanupResult.deletedAttachmentRecords !== 1 ? 's' : ''} )}
{cleanupResult.errors.length > 0 && (
{cleanupResult.errors.length} error{cleanupResult.errors.length !== 1 ? 's' : ''}: {cleanupResult.errors[0]}
)}
)}
{/* Error / Refresh */} {error && (
{error}
)}
); }