import React, { useState, useEffect, useCallback } from 'react'; import { Database, Download, Play, RefreshCw, RotateCcw, Trash2, Upload } from 'lucide-react'; import { toast } from 'sonner'; import { api } from '@/api'; import { cn, fmtBytes } from '@/lib/utils'; import { Button, buttonVariants } from '@/components/ui/button'; import { Badge } from '@/components/ui/badge'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Card, CardHeader, CardTitle, CardContent } from '@/components/ui/card'; import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem, } from '@/components/ui/select'; import { AlertDialog, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction, } from '@/components/ui/alert-dialog'; import { SectionHeading, Toggle, formatDateTime, BackupTypeBadge } from './adminShared'; const DEFAULT_SETTINGS = { enabled: false, frequency: 'daily', time: '02:00', retention_count: 2, last_run_at: null, next_run_at: null, last_error: null, }; export default function BackupManagementCard() { const [backups, setBackups] = useState([]); const [settings, setSettings] = useState(DEFAULT_SETTINGS); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(''); const [restoreTarget, setRestoreTarget] = useState(null); const [deleteTarget, setDeleteTarget] = useState(null); const load = useCallback(async () => { setLoading(true); try { const [backupData, settingsData] = await Promise.all([ api.adminBackups(), api.adminBackupSettings(), ]); setBackups(backupData.backups || []); setSettings({ ...DEFAULT_SETTINGS, ...settingsData }); } catch (err) { toast.error(err.message || 'Failed to load backups.'); } finally { setLoading(false); } }, []); useEffect(() => { load(); }, [load]); const latest = backups[0]; const setSchedule = (key, value) => setSettings(prev => ({ ...prev, [key]: value })); async function handleCreate() { setBusy('create'); try { await api.createAdminBackup(); toast.success('Backup created.'); await load(); } catch (err) { toast.error(err.message || 'Failed to create backup.'); } finally { setBusy(''); } } async function handleDownload(backup) { setBusy(`download:${backup.id}`); try { const { blob, filename } = await api.downloadAdminBackup(backup.id); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = filename || backup.id; document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url); } catch (err) { toast.error(err.message || 'Failed to download backup.'); } finally { setBusy(''); } } async function handleImport(e) { const file = e.target.files?.[0]; e.target.value = ''; if (!file) return; setBusy('import'); try { await api.importAdminBackup(file); toast.success('Backup imported.'); await load(); } catch (err) { toast.error(err.message || 'Failed to import backup.'); } finally { setBusy(''); } } async function handleRestore() { if (!restoreTarget) return; setBusy(`restore:${restoreTarget.id}`); try { const result = await api.restoreAdminBackup(restoreTarget.id); toast.success(`Database restored. Pre-restore backup: ${result.pre_restore_backup}`); setRestoreTarget(null); await load(); } catch (err) { toast.error(err.message || 'Failed to restore backup.'); } finally { setBusy(''); } } async function handleDelete() { if (!deleteTarget) return; setBusy(`delete:${deleteTarget.id}`); try { await api.deleteAdminBackup(deleteTarget.id); toast.success('Backup deleted.'); setDeleteTarget(null); await load(); } catch (err) { toast.error(err.message || 'Failed to delete backup.'); } finally { setBusy(''); } } async function handleSaveSettings() { setBusy('settings'); try { const saved = await api.saveAdminBackupSettings({ enabled: !!settings.enabled, frequency: settings.frequency, time: settings.time, retention_count: parseInt(settings.retention_count, 10) || 2, }); setSettings({ ...DEFAULT_SETTINGS, ...saved }); toast.success('Backup schedule saved.'); } catch (err) { toast.error(err.message || 'Failed to save backup schedule.'); } finally { setBusy(''); } } async function handleRunScheduledNow() { setBusy('run-scheduled'); try { await api.runScheduledBackupNow(); toast.success('Scheduled backup created.'); await load(); } catch (err) { toast.error(err.message || 'Failed to run scheduled backup.'); } finally { setBusy(''); } } if (loading) { return Loading backups…; } return ( <>
Database Backups

Admin-only SQLite backup, import, download, restore, and schedule controls.

{settings.last_error ? 'Attention' : 'Ready'}
{[ ['Managed backups', backups.length], ['Latest backup', latest ? formatDateTime(latest.modified_at) : '—'], ['Latest size', latest ? fmtBytes(latest.size_bytes) : '—'], ['Scheduled', settings.enabled ? `${settings.frequency} ${settings.time}` : 'Disabled'], ].map(([label, value]) => (

{label}

{value}

))}
{settings.last_error && (
{settings.last_error}
)}
{backups.map(backup => ( ))} {!backups.length && ( )}
Backup Type Modified Size Checksum
{backup.id} {formatDateTime(backup.modified_at)} {fmtBytes(backup.size_bytes)} {backup.checksum || '—'}
No managed backups yet.
Scheduled Backups

Scheduled runs create managed backups and only apply retention to scheduled backups.

setSchedule('enabled', v)} label="Enable scheduled backups" />
setSchedule('time', e.target.value)} />
setSchedule('retention_count', e.target.value)} />
{formatDateTime(settings.next_run_at)}
Last run: {formatDateTime(settings.last_run_at)}
Last error: {settings.last_error || '—'}
{ if (!open) setRestoreTarget(null); }}> Restore this database backup? This replaces the live database with {restoreTarget?.id}. A pre-restore backup will be created first. Run this during a quiet maintenance window. Cancel {busy.startsWith('restore:') ? 'Restoring…' : 'Restore Database'} { if (!open) setDeleteTarget(null); }}> Delete this backup? This permanently deletes {deleteTarget?.id}. The live database is not affected. Cancel {busy.startsWith('delete:') ? 'Deleting…' : 'Delete Backup'} ); }