From cc79b3d9ec6ff412f69ab04d24af77f9285e738d Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Thu, 2 Jul 2026 20:37:35 +0200 Subject: [PATCH] refactor(backup): move .picpeak download to the Dashboard tab Downloading a portable backup is a "make a backup" action, so it belongs next to "Run Backup Now" on the Dashboard, not under Restore. Split the combined card into PicpeakExportCard (Dashboard) and PicpeakRestoreCard (Restore). The manifest stays bundled inside the .picpeak, so there is no separate manifest-only download for the portable format. --- .../components/admin/PicpeakBackupCard.tsx | 199 +++++++++--------- frontend/src/pages/admin/BackupManagement.tsx | 19 +- 2 files changed, 115 insertions(+), 103 deletions(-) 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')} />
)}