- Add backup types and RecallBackup format - Create backup snapshot engine (createBackupSnapshot) - Add IndexedDB storage for local backups - Implement retention policy (max 10, 30-day cleanup) - Add backup validation and restore logic (merge/replace modes) - Add backup restore UI dialog with preview and confirmation - Add unsaved changes guard hook - Integrate backups section in Settings - Add backup endpoint to export-import API
137 lines
4.0 KiB
TypeScript
137 lines
4.0 KiB
TypeScript
'use client'
|
|
import { useState, useEffect } from 'react'
|
|
import { getBackups, deleteBackup } from '@/lib/backup-storage'
|
|
import { RecallBackup } from '@/types/backup'
|
|
import { Button } from '@/components/ui/button'
|
|
import { Badge } from '@/components/ui/badge'
|
|
import { toast } from 'sonner'
|
|
import { Trash2, RotateCcw, Calendar, FileText } from 'lucide-react'
|
|
import { BackupRestoreDialog } from './backup-restore-dialog'
|
|
|
|
export function BackupList() {
|
|
const [backups, setBackups] = useState<RecallBackup[]>([])
|
|
const [loading, setLoading] = useState(true)
|
|
const [deletingId, setDeletingId] = useState<string | null>(null)
|
|
|
|
useEffect(() => {
|
|
loadBackups()
|
|
}, [])
|
|
|
|
async function loadBackups() {
|
|
try {
|
|
const data = await getBackups()
|
|
setBackups(data)
|
|
} catch {
|
|
toast.error('Error al cargar los backups')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
async function handleDelete(id: string) {
|
|
setDeletingId(id)
|
|
try {
|
|
await deleteBackup(id)
|
|
setBackups((prev) => prev.filter((b) => b.id !== id))
|
|
toast.success('Backup eliminado')
|
|
} catch {
|
|
toast.error('Error al eliminar el backup')
|
|
} finally {
|
|
setDeletingId(null)
|
|
}
|
|
}
|
|
|
|
function formatDate(dateStr: string) {
|
|
return new Date(dateStr).toLocaleString('es-ES', {
|
|
dateStyle: 'medium',
|
|
timeStyle: 'short',
|
|
})
|
|
}
|
|
|
|
function getSourceBadgeVariant(source: RecallBackup['source']) {
|
|
switch (source) {
|
|
case 'automatic':
|
|
return 'secondary'
|
|
case 'manual':
|
|
return 'default'
|
|
case 'pre-destructive':
|
|
return 'destructive'
|
|
default:
|
|
return 'secondary'
|
|
}
|
|
}
|
|
|
|
function getSourceLabel(source: RecallBackup['source']) {
|
|
switch (source) {
|
|
case 'automatic':
|
|
return 'Automático'
|
|
case 'manual':
|
|
return 'Manual'
|
|
case 'pre-destructive':
|
|
return 'Pre-destrucción'
|
|
default:
|
|
return source
|
|
}
|
|
}
|
|
|
|
if (loading) {
|
|
return <div className="text-sm text-muted-foreground">Cargando backups...</div>
|
|
}
|
|
|
|
if (backups.length === 0) {
|
|
return (
|
|
<div className="text-sm text-muted-foreground">
|
|
No hay backups disponibles. Los backups se crean automáticamente antes de operaciones
|
|
destructivas.
|
|
</div>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<div className="space-y-3">
|
|
{backups.map((backup) => (
|
|
<div
|
|
key={backup.id}
|
|
className="flex items-center justify-between p-3 border rounded-lg"
|
|
>
|
|
<div className="flex-1 min-w-0">
|
|
<div className="flex items-center gap-2 mb-1">
|
|
<Calendar className="h-4 w-4 text-muted-foreground" />
|
|
<span className="text-sm font-medium">{formatDate(backup.createdAt)}</span>
|
|
<Badge variant={getSourceBadgeVariant(backup.source)}>{getSourceLabel(backup.source)}</Badge>
|
|
</div>
|
|
<div className="flex items-center gap-4 text-xs text-muted-foreground">
|
|
<span className="flex items-center gap-1">
|
|
<FileText className="h-3 w-3" />
|
|
{backup.metadata.noteCount} nota{backup.metadata.noteCount !== 1 ? 's' : ''}
|
|
</span>
|
|
<span>
|
|
{backup.metadata.tagCount} tag{backup.metadata.tagCount !== 1 ? 's' : ''}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
<div className="flex items-center gap-2 ml-4">
|
|
<BackupRestoreDialog
|
|
backup={backup}
|
|
trigger={
|
|
<Button variant="outline" size="sm" className="gap-1 cursor-pointer">
|
|
<RotateCcw className="h-3 w-3" />
|
|
Restaurar
|
|
</Button>
|
|
}
|
|
/>
|
|
<Button
|
|
variant="ghost"
|
|
size="icon"
|
|
onClick={() => handleDelete(backup.id)}
|
|
disabled={deletingId === backup.id}
|
|
>
|
|
<Trash2 className="h-4 w-4 text-muted-foreground" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|