feat: MVP-5 Sprint 1 - Backup/Restore system

- 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
This commit is contained in:
2026-03-22 18:16:36 -03:00
parent 544decf4ac
commit 8c80a12b81
14 changed files with 1824 additions and 5 deletions

View File

@@ -0,0 +1,75 @@
import { RecallBackup } from '@/types/backup'
interface ValidationResult {
valid: boolean
errors: string[]
info?: {
noteCount: number
tagCount: number
createdAt: string
source: string
}
}
export function validateBackup(data: unknown): ValidationResult {
const errors: string[] = []
if (typeof data !== 'object' || data === null) {
return { valid: false, errors: ['Backup must be an object'] }
}
const backup = data as Record<string, unknown>
if (!validateSchemaVersion(backup as unknown as RecallBackup)) {
errors.push('Missing or invalid schemaVersion (expected "1.0")')
}
if (!backup.createdAt || typeof backup.createdAt !== 'string') {
errors.push('Missing or invalid createdAt field')
}
if (!backup.source || typeof backup.source !== 'string') {
errors.push('Missing or invalid source field')
}
if (!backup.metadata || typeof backup.metadata !== 'object') {
errors.push('Missing or invalid metadata field')
} else {
const metadata = backup.metadata as Record<string, unknown>
if (typeof metadata.noteCount !== 'number') {
errors.push('Missing or invalid metadata.noteCount')
}
if (typeof metadata.tagCount !== 'number') {
errors.push('Missing or invalid metadata.tagCount')
}
}
if (!backup.data || typeof backup.data !== 'object') {
errors.push('Missing or invalid data field')
} else {
const data = backup.data as Record<string, unknown>
if (!Array.isArray(data.notes)) {
errors.push('Missing or invalid data.notes (expected array)')
}
}
if (errors.length > 0) {
return { valid: false, errors }
}
const metadata = backup.metadata as { noteCount: number; tagCount: number }
return {
valid: true,
errors: [],
info: {
noteCount: metadata.noteCount,
tagCount: metadata.tagCount,
createdAt: backup.createdAt as string,
source: backup.source as string,
},
}
}
export function validateSchemaVersion(backup: RecallBackup): boolean {
return backup.schemaVersion === '1.0'
}