import React, { useRef, useState } from 'react'; import { Download, Upload, AlertTriangle, ShieldAlert, ExternalLink, CheckCircle2 } from 'lucide-react'; import { toast } from 'react-toastify'; import { useTranslation } from 'react-i18next'; import { Button, Card } from '../common'; import { api } from '../../config/api'; // Portable ".picpeak" roundtrip, split across two Backup Manager tabs: // - PicpeakExportCard → Dashboard (making a backup) // - PicpeakRestoreCard → Restore (restoring a backup) // The manifest is bundled inside the .picpeak, so there is no separate // "manifest only" download here. interface RestoreResult { tables: number; filesRestored: number; usesExternalMedia: boolean; sessionInvalidated?: boolean; } // ── Download half (Dashboard) ──────────────────────────────────────────────── export const PicpeakExportCard: React.FC = () => { const { t } = useTranslation(); const [includePhotos, setIncludePhotos] = useState(false); const [downloading, setDownloading] = useState(false); const handleDownload = async () => { setDownloading(true); try { const res = await api.get('/admin/backup/picpeak/export', { params: { includePhotos }, responseType: 'blob', }); const cd = (res.headers['content-disposition'] as string) || ''; const match = cd.match(/filename="?([^"]+)"?/); const filename = (match && match[1]) || 'picpeak-backup.picpeak'; const url = window.URL.createObjectURL(res.data as Blob); const a = document.createElement('a'); a.href = url; a.download = filename; document.body.appendChild(a); a.click(); a.remove(); window.URL.revokeObjectURL(url); } catch (_) { toast.error(t('backup.picpeak.downloadFailed', 'Could not create the backup file.')); } finally { setDownloading(false); } }; return (

{t('backup.picpeak.title', 'Portable backup (.picpeak)')}

{t('backup.picpeak.intro', 'Download a single self-contained file, then upload it on another instance to clone this one — all through the browser.')}

{t('backup.picpeak.secretsWarning', 'This file contains secrets in plain text (email password, admin credentials, API keys). Store it securely and only transfer it over trusted channels.')}

); }; PicpeakExportCard.displayName = 'PicpeakExportCard'; // ── Restore half (Restore tab) ─────────────────────────────────────────────── export const PicpeakRestoreCard: React.FC = () => { const { t } = useTranslation(); const fileRef = useRef(null); const [pendingFile, setPendingFile] = useState(null); const [restoring, setRestoring] = useState(false); const [result, setResult] = useState(null); const onFilePick = (e: React.ChangeEvent) => { const f = e.target.files?.[0]; if (f) setPendingFile(f); e.target.value = ''; // let the user re-pick the same file after cancelling }; const confirmRestore = async () => { if (!pendingFile) return; setRestoring(true); try { const fd = new FormData(); fd.append('backup', pendingFile); const res = await api.post('/admin/backup/picpeak/import', fd); setResult(res.data); setPendingFile(null); toast.success(t('backup.picpeak.restoreDone', 'Backup restored.')); // The restore rewrote admin_users and the backend revoked our session // (ids may have shifted). Send the operator to a fresh login rather than // letting the now-stale token resolve to a different restored account. if (res.data?.sessionInvalidated) { toast.success(t('backup.picpeak.reloginRequired', 'Restore complete — please sign in again.')); setTimeout(() => { window.location.href = '/admin/login'; }, 1500); } } catch (e: any) { const msg = e.response?.data?.error || t('backup.picpeak.restoreFailed', 'Restore failed.'); toast.error(msg); setPendingFile(null); } finally { setRestoring(false); } }; return (

{t('backup.picpeak.restoreTitle', 'Restore from a .picpeak')}

{t('backup.picpeak.restoreIntro', 'Upload a .picpeak taken from this or another instance. Same database engine only.')}

{result && (

{t('backup.picpeak.restoreDone', 'Backup restored.')}

{t('backup.picpeak.restoreSummary', '{{tables}} tables and {{files}} files restored.', { tables: result.tables, files: result.filesRestored, })}

{result.usesExternalMedia && (

{t('backup.picpeak.externalMediaNote', 'This backup references an external-media library. Make sure external-media routing is configured on this instance.')}{' '} {t('backup.picpeak.externalMediaLink', 'Setup guide')}

)}
)} {/* Destructive confirmation */} {pendingFile && (

{t('backup.picpeak.confirmTitle', 'Restore will delete all current data')}

{t('backup.picpeak.confirmBody', 'This permanently replaces ALL data on this instance with the uploaded backup, except your current account. This cannot be undone.')}

{pendingFile.name}

)}
); }; PicpeakRestoreCard.displayName = 'PicpeakRestoreCard';