diff --git a/frontend/src/components/admin/PicpeakBackupCard.tsx b/frontend/src/components/admin/PicpeakBackupCard.tsx index de8072f3..10ae1e60 100644 --- a/frontend/src/components/admin/PicpeakBackupCard.tsx +++ b/frontend/src/components/admin/PicpeakBackupCard.tsx @@ -6,23 +6,23 @@ 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; } -// Portable ".picpeak" roundtrip: download a self-contained backup here, upload -// it on another instance to clone this one. Restore is a FULL OVERRIDE (all data -// replaced) that keeps only the current account — hence the explicit confirm. -export const PicpeakBackupCard: React.FC = () => { +// ── Download half (Dashboard) ──────────────────────────────────────────────── +export const PicpeakExportCard: React.FC = () => { const { t } = useTranslation(); - const fileRef = useRef(null); const [includePhotos, setIncludePhotos] = useState(false); const [downloading, setDownloading] = useState(false); - const [pendingFile, setPendingFile] = useState(null); - const [restoring, setRestoring] = useState(false); - const [result, setResult] = useState(null); const handleDownload = async () => { setDownloading(true); @@ -49,6 +49,55 @@ export const PicpeakBackupCard: React.FC = () => { } }; + 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); @@ -77,99 +126,59 @@ export const PicpeakBackupCard: React.FC = () => { return (

- {t('backup.picpeak.title', 'Portable backup (.picpeak)')} + {t('backup.picpeak.restoreTitle', 'Restore from a .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.restoreIntro', 'Upload a .picpeak taken from this or another instance. Same database engine only.')}

+ + - {/* Download */} -
- -
- -

- {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.')} -

-
- -
- -
- - {/* Restore */} -
-

- {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.')} + {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')} + + +

-

- {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 && ( @@ -207,4 +216,4 @@ export const PicpeakBackupCard: React.FC = () => { ); }; -PicpeakBackupCard.displayName = 'PicpeakBackupCard'; +PicpeakRestoreCard.displayName = 'PicpeakRestoreCard'; diff --git a/frontend/src/pages/admin/BackupManagement.tsx b/frontend/src/pages/admin/BackupManagement.tsx index d0d74586..b72d3acd 100644 --- a/frontend/src/pages/admin/BackupManagement.tsx +++ b/frontend/src/pages/admin/BackupManagement.tsx @@ -23,7 +23,7 @@ import { BackupDashboard } from '../../components/admin/BackupDashboard'; import { BackupConfiguration } from '../../components/admin/BackupConfiguration'; import { BackupHistory } from '../../components/admin/BackupHistory'; import { RestoreWizard } from '../../components/admin/RestoreWizard'; -import { PicpeakBackupCard } from '../../components/admin/PicpeakBackupCard'; +import { PicpeakExportCard, PicpeakRestoreCard } from '../../components/admin/PicpeakBackupCard'; import { BackupIntegrityCard } from '../../components/admin/BackupIntegrityCard'; import { BackupCoverageCard } from '../../components/admin/BackupCoverageCard'; import { api } from '../../config/api'; @@ -205,12 +205,15 @@ export const BackupManagement: React.FC = () => { {/* Tab Content */}
{activeTab === 'dashboard' && ( - manualBackupMutation.mutate()} - isBackupRunning={backupStatus?.isRunning || manualBackupMutation.isPending} - /> +
+ manualBackupMutation.mutate()} + isBackupRunning={backupStatus?.isRunning || manualBackupMutation.isPending} + /> + +
)} {activeTab === 'configuration' && ( @@ -227,7 +230,7 @@ export const BackupManagement: React.FC = () => { {activeTab === 'restore' && (
- + setActiveTab('integrity')} />
)}