import { useState, useEffect, useRef } from 'react' import { backupApi } from '../../api/client' import { useToast } from '../shared/Toast' import { Download, Trash2, Plus, RefreshCw, RotateCcw, Upload, Clock, Check, HardDrive, AlertTriangle } from 'lucide-react' import { useTranslation } from '../../i18n' import { useSettingsStore } from '../../store/settingsStore' import { getApiErrorMessage } from '../../types' const INTERVAL_OPTIONS = [ { value: 'hourly', labelKey: 'backup.interval.hourly' }, { value: 'daily', labelKey: 'backup.interval.daily' }, { value: 'weekly', labelKey: 'backup.interval.weekly' }, { value: 'monthly', labelKey: 'backup.interval.monthly' }, ] const KEEP_OPTIONS = [ { value: 1, labelKey: 'backup.keep.1day' }, { value: 3, labelKey: 'backup.keep.3days' }, { value: 7, labelKey: 'backup.keep.7days' }, { value: 14, labelKey: 'backup.keep.14days' }, { value: 30, labelKey: 'backup.keep.30days' }, { value: 0, labelKey: 'backup.keep.forever' }, ] const DAYS_OF_WEEK = [ { value: 0, labelKey: 'backup.dow.sunday' }, { value: 1, labelKey: 'backup.dow.monday' }, { value: 2, labelKey: 'backup.dow.tuesday' }, { value: 3, labelKey: 'backup.dow.wednesday' }, { value: 4, labelKey: 'backup.dow.thursday' }, { value: 5, labelKey: 'backup.dow.friday' }, { value: 6, labelKey: 'backup.dow.saturday' }, ] const HOURS = Array.from({ length: 24 }, (_, i) => i) const DAYS_OF_MONTH = Array.from({ length: 28 }, (_, i) => i + 1) export default function BackupPanel() { const [backups, setBackups] = useState([]) const [isLoading, setIsLoading] = useState(false) const [isCreating, setIsCreating] = useState(false) const [restoringFile, setRestoringFile] = useState(null) const [isUploading, setIsUploading] = useState(false) const [autoSettings, setAutoSettings] = useState({ enabled: false, interval: 'daily', keep_days: 7, hour: 2, day_of_week: 0, day_of_month: 1 }) const [autoSettingsSaving, setAutoSettingsSaving] = useState(false) const [autoSettingsDirty, setAutoSettingsDirty] = useState(false) const [serverTimezone, setServerTimezone] = useState('') const [restoreConfirm, setRestoreConfirm] = useState(null) // { type: 'file'|'upload', filename, file? } const fileInputRef = useRef(null) const toast = useToast() const { t, language, locale } = useTranslation() const is12h = useSettingsStore(s => s.settings.time_format) === '12h' const loadBackups = async () => { setIsLoading(true) try { const data = await backupApi.list() setBackups(data.backups || []) } catch { toast.error(t('backup.toast.loadError')) } finally { setIsLoading(false) } } const loadAutoSettings = async () => { try { const data = await backupApi.getAutoSettings() setAutoSettings(data.settings) if (data.timezone) setServerTimezone(data.timezone) } catch {} } useEffect(() => { loadBackups(); loadAutoSettings() }, []) const handleCreate = async () => { setIsCreating(true) try { await backupApi.create() toast.success(t('backup.toast.created')) await loadBackups() } catch { toast.error(t('backup.toast.createError')) } finally { setIsCreating(false) } } const handleRestore = (filename) => { setRestoreConfirm({ type: 'file', filename }) } const handleUploadRestore = (e) => { const file = (e.target as HTMLInputElement).files?.[0] if (!file) return e.target.value = '' setRestoreConfirm({ type: 'upload', filename: file.name, file }) } const executeRestore = async () => { if (!restoreConfirm) return const { type, filename, file } = restoreConfirm setRestoreConfirm(null) if (type === 'file') { setRestoringFile(filename) try { await backupApi.restore(filename) toast.success(t('backup.toast.restored')) setTimeout(() => window.location.reload(), 1500) } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('backup.toast.restoreError'))) setRestoringFile(null) } } else { setIsUploading(true) try { await backupApi.uploadRestore(file) toast.success(t('backup.toast.restored')) setTimeout(() => window.location.reload(), 1500) } catch (err: unknown) { toast.error(getApiErrorMessage(err, t('backup.toast.uploadError'))) setIsUploading(false) } } } const handleDelete = async (filename) => { if (!confirm(t('backup.confirm.delete', { name: filename }))) return try { await backupApi.delete(filename) toast.success(t('backup.toast.deleted')) setBackups(prev => prev.filter(b => b.filename !== filename)) } catch { toast.error(t('backup.toast.deleteError')) } } const handleAutoSettingsChange = (key, value) => { setAutoSettings(prev => ({ ...prev, [key]: value })) setAutoSettingsDirty(true) } const handleSaveAutoSettings = async () => { setAutoSettingsSaving(true) try { const data = await backupApi.setAutoSettings(autoSettings) setAutoSettings(data.settings) setAutoSettingsDirty(false) toast.success(t('backup.toast.settingsSaved')) } catch { toast.error(t('backup.toast.settingsError')) } finally { setAutoSettingsSaving(false) } } const formatSize = (bytes) => { if (!bytes) return '-' if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` return `${(bytes / 1024 / 1024).toFixed(1)} MB` } const formatDate = (dateStr) => { if (!dateStr) return '-' try { const opts: Intl.DateTimeFormatOptions = { day: '2-digit', month: '2-digit', year: 'numeric', hour: '2-digit', minute: '2-digit', } if (serverTimezone) opts.timeZone = serverTimezone return new Date(dateStr).toLocaleString(locale, opts) } catch { return dateStr } } const isAuto = (filename) => filename.startsWith('auto-backup-') return (
{/* Manual Backups */}

{t('backup.title')}

{t('backup.subtitle')}

{/* Upload & Restore */}
{isLoading && backups.length === 0 ? (
{t('common.loading')}
) : backups.length === 0 ? (

{t('backup.empty')}

) : (
{backups.map(backup => (
{isAuto(backup.filename) ? : }

{backup.filename}

{isAuto(backup.filename) && ( Auto )}
{formatDate(backup.created_at)} {formatSize(backup.size)}
))}
)}
{/* Auto-Backup Settings */}

{t('backup.auto.title')}

{t('backup.auto.subtitle')}

{/* Enable toggle */} {autoSettings.enabled && ( <> {/* Interval */}
{INTERVAL_OPTIONS.map(opt => ( ))}
{/* Hour picker (for daily, weekly, monthly) */} {autoSettings.interval !== 'hourly' && (

{t('backup.auto.hourHint', { format: is12h ? '12h' : '24h' })}{serverTimezone ? ` (Timezone: ${serverTimezone})` : ''}

)} {/* Day of week (for weekly) */} {autoSettings.interval === 'weekly' && (
{DAYS_OF_WEEK.map(opt => ( ))}
)} {/* Day of month (for monthly) */} {autoSettings.interval === 'monthly' && (

{t('backup.auto.dayOfMonthHint')}

)} {/* Keep duration */}
{KEEP_OPTIONS.map(opt => ( ))}
)} {/* Save button */}
{/* Restore Warning Modal */} {restoreConfirm && (
setRestoreConfirm(null)} >
e.stopPropagation()} style={{ width: '100%', maxWidth: 440, borderRadius: 16, overflow: 'hidden' }} className="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700" > {/* Red header */}

{t('backup.restoreConfirmTitle')}

{restoreConfirm.filename}

{/* Body */}

{t('backup.restoreWarning')}

{t('backup.restoreTip')}
{/* Footer */}
)}
) }