diff --git a/frontend/src/components/admin/RestoreWizard.jsx b/frontend/src/components/admin/RestoreWizard.jsx index 3269e44c..31d152ab 100644 --- a/frontend/src/components/admin/RestoreWizard.jsx +++ b/frontend/src/components/admin/RestoreWizard.jsx @@ -338,15 +338,68 @@ export const RestoreWizard = ({ onVerifyIntegrity } = {}) => {

- {backup.encrypted && ( - - )} +
+ {/* Files-only warning — backend's /list-backups now + returns `database_included: boolean` parsed from + the manifest's database.backup_file field. A row + where this is false is exactly the data-loss + scenario the Stage A guard prevents going forward: + a manifest written without an inline DB dump. + Restoring it would NOT bring CRM data back. */} + {backup.database_included === false && ( + + + {t('backup.restore.backup.filesOnlyBadge', 'No DB')} + + )} + {backup.corrupt && ( + + + {t('backup.restore.backup.corruptBadge', 'Corrupt')} + + )} + {backup.encrypted && ( + + )} +
))} )} + {/* Files-only callout below the selected card. Reinforces the + badge with a longer explanation + reminds the admin that + restoring this WILL still proceed — they just won't get the + DB back. Stops the silent-failure class that originally + caused Ralf's 2026-05-29 data loss (four files-only manifests + mistaken for full backups). */} + {restoreData.selectedBackup && restoreData.selectedBackup.database_included === false && ( + +
+ +
+

+ {t('backup.restore.backup.filesOnlyWarning.title', + 'Selected backup has no database dump')} +

+

+ {t('backup.restore.backup.filesOnlyWarning.message', + 'Restoring this backup will recover files (photos, PDFs) but the database — including admin users, customers, quotes, invoices, contracts, and settings — will NOT come back. Pick a different backup if you have one with a database dump, or proceed only if files-only is what you want.')} +

+
+
+
+ )} + {restoreData.selectedBackup?.encrypted && (
@@ -574,16 +627,63 @@ export const RestoreWizard = ({ onVerifyIntegrity } = {}) => { const renderProgress = () => { const progress = restoreStatus?.currentProgress || {}; const isRunning = restoreStatus?.isRunning; + // Pull the most recent restore_runs row from history so we can + // tell whether the "not running" state means success, failure, or + // never-started. The history endpoint already returns rows newest + // first. + const lastRun = restoreStatus?.history?.[0]; + const lastRunFailed = + !isRunning && lastRun && (lastRun.status === 'failed' || lastRun.was_successful === false); + const lastRunSucceeded = + !isRunning && lastRun && lastRun.status === 'completed' && lastRun.was_successful === true; + // Strip the noisy stack-trace tail from the error message so the + // user sees the actionable line first. + const lastRunError = lastRun?.error_message + ? lastRun.error_message.split('\n')[0].slice(0, 500) + : null; + const subtitle = isRunning + ? t('backup.restore.progress.inProgress') + : lastRunFailed + ? t('backup.restore.progress.failedSubtitle', 'Restore failed — see error below. Destination has been rolled back to its pre-restore state.') + : lastRunSucceeded + ? t('backup.restore.progress.completed') + : t('backup.restore.progress.idle', 'No restore in progress.'); return (

{t('backup.restore.progress.title')}

-

- {isRunning ? t('backup.restore.progress.inProgress') : t('backup.restore.progress.completed')} +

+ {subtitle}

+ {lastRunFailed && ( +
+
+ +
+

+ {t('backup.restore.progress.errorTitle', 'Restore did not complete')} +

+

+ {lastRunError || t('backup.restore.progress.errorUnknown', 'No error message recorded.')} +

+ {lastRun.was_rollback_attempted && ( +

+ {t('backup.restore.progress.rolledBack', + 'Pre-restore safety backup was used to roll back. Destination is in its pre-restore state — safe to retry once the issue above is resolved.')} +

+ )} +
+
+
+ )} + {/* Progress Bar */}
@@ -646,8 +746,11 @@ export const RestoreWizard = ({ onVerifyIntegrity } = {}) => { )} - {/* Completion Actions */} - {!isRunning && progress.status === 'completed' && ( + {/* Completion Actions — only when the most recent run actually + succeeded. Previously this gated on `progress.status` which + could be null between runs, so the green "Restore completed + successfully" banner could render alongside a silent failure. */} + {lastRunSucceeded && (