import { useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { Archive, Download, Upload, Check, AlertTriangle } from 'lucide-react' import { useDialog } from './ModalDialog.tsx' import { downloadBackupBlob, exportLogbookBackup, parseLogbookBackupFile, previewLogbookBackup, restoreLogbookBackup, type LogbookBackupFile, type LogbookBackupPreview } from '../services/logbookBackup.js' import { PlausibleEvents, trackPlausibleEvent } from '../services/analytics.js' import { formatAppDateTime } from '../utils/dateTimeFormat.js' interface LogbookBackupPanelProps { logbookId: string onRestored?: (logbookId: string, title: string) => void } function mapBackupError(code: string, t: (key: string) => string): string { switch (code) { case 'BACKUP_PASSPHRASE_TOO_SHORT': return t('settings.backup_passphrase_short') case 'BACKUP_NOT_OWNER': return t('settings.backup_not_owner') case 'BACKUP_INVALID_JSON': return t('settings.backup_invalid_json') case 'BACKUP_INVALID_FORMAT': return t('settings.backup_invalid_format') case 'BACKUP_NOT_AUTHENTICATED': return t('settings.backup_not_authenticated') case 'BACKUP_ID_CONFLICT': return t('settings.backup_id_conflict') default: if (code.includes('decrypt') || code.includes('operation')) { return t('settings.backup_wrong_passphrase') } return code } } export default function LogbookBackupPanel({ logbookId, onRestored }: LogbookBackupPanelProps) { const { t, i18n } = useTranslation() const { showConfirm } = useDialog() const fileInputRef = useRef(null) const [exportPassphrase, setExportPassphrase] = useState('') const [exportConfirm, setExportConfirm] = useState('') const [exporting, setExporting] = useState(false) const [importPassphrase, setImportPassphrase] = useState('') const [importFile, setImportFile] = useState(null) const [importPreview, setImportPreview] = useState(null) const [parsedBackup, setParsedBackup] = useState(null) const [importing, setImporting] = useState(false) const [previewing, setPreviewing] = useState(false) const [error, setError] = useState(null) const [success, setSuccess] = useState(null) const handleExportSubmit = async (e: React.FormEvent) => { e.preventDefault() await handleExport() } const handleImportSubmit = async (e: React.FormEvent) => { e.preventDefault() await handleRestore() } const handleExport = async () => { setError(null) setSuccess(null) if (exportPassphrase.length < 8) { setError(t('settings.backup_passphrase_short')) return } if (exportPassphrase !== exportConfirm) { setError(t('settings.backup_passphrase_mismatch')) return } setExporting(true) try { const { blob, filename, backup } = await exportLogbookBackup(logbookId, exportPassphrase) downloadBackupBlob(blob, filename) setSuccess(t('settings.backup_export_success', { count: backup.counts.entries })) setExportPassphrase('') setExportConfirm('') trackPlausibleEvent(PlausibleEvents.BACKUP_EXPORTED, { entries: backup.counts.entries, photos: backup.counts.photos }) } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err) setError(mapBackupError(message, t)) } finally { setExporting(false) } } const handleFileChange = async (e: React.ChangeEvent) => { setError(null) setSuccess(null) setImportPreview(null) setParsedBackup(null) const file = e.target.files?.[0] setImportFile(file ?? null) if (!file) return try { const backup = await parseLogbookBackupFile(file) setParsedBackup(backup) } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err) setError(mapBackupError(message, t)) setImportFile(null) } } const handlePreviewImport = async () => { if (!parsedBackup || !importPassphrase) return setPreviewing(true) setError(null) try { const preview = await previewLogbookBackup(parsedBackup, importPassphrase) setImportPreview(preview) } catch (err: unknown) { setImportPreview(null) setError(t('settings.backup_wrong_passphrase')) } finally { setPreviewing(false) } } const handleRestore = async (options: { overwrite?: boolean; assignNewId?: boolean } = {}) => { if (!parsedBackup || !importPassphrase) return setImporting(true) setError(null) try { const result = await restoreLogbookBackup(parsedBackup, importPassphrase, options) setSuccess(t('settings.backup_restore_success', { title: result.title })) setImportFile(null) setImportPassphrase('') setImportPreview(null) setParsedBackup(null) if (fileInputRef.current) fileInputRef.current.value = '' trackPlausibleEvent(PlausibleEvents.BACKUP_RESTORED, { entries: parsedBackup.counts.entries, photos: parsedBackup.counts.photos, mode: options.overwrite ? 'overwrite' : options.assignNewId ? 'new_id' : 'same_id' }) onRestored?.(result.logbookId, result.title) } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err) if (message === 'BACKUP_ID_CONFLICT') { const overwrite = await showConfirm( t('settings.backup_overwrite_confirm'), t('settings.backup_restore_title'), t('logs.confirm_yes'), t('logs.confirm_no') ) if (overwrite) { setImporting(false) return handleRestore({ overwrite: true }) } const asNew = await showConfirm( t('settings.backup_new_id_confirm'), t('settings.backup_restore_title'), t('logs.confirm_yes'), t('logs.confirm_no') ) if (asNew) { setImporting(false) return handleRestore({ assignNewId: true }) } setError(t('settings.backup_restore_cancelled')) } else { setError(mapBackupError(message, t)) } } finally { setImporting(false) } } return (

{t('settings.backup_title')}

{t('settings.backup_desc')}

{error && (
{error}
)} {success && (
{success}
)}

{t('settings.backup_export_desc')}

setExportPassphrase(e.target.value)} placeholder={t('settings.backup_passphrase_placeholder')} autoComplete="new-password" disabled={exporting} required />
setExportConfirm(e.target.value)} autoComplete="new-password" disabled={exporting} required />

{t('settings.backup_restore_desc')}

{importFile && ( <>
{ setImportPassphrase(e.target.value) setImportPreview(null) }} autoComplete="current-password" disabled={importing} required />
)}
{importPreview && (

{importPreview.title}

  • {t('settings.backup_stat_entries', { count: importPreview.counts.entries })}
  • {t('settings.backup_stat_photos', { count: importPreview.counts.photos })}
  • {t('settings.backup_stat_crew', { count: importPreview.counts.crews })}
  • {t('settings.backup_stat_tracks', { count: importPreview.counts.gpsTracks })}

{t('settings.backup_exported_at', { date: formatAppDateTime(importPreview.exportedAt, i18n.language) })}

)}
) }