From 47ed6907d141de883e95377665fdd61527bba107 Mon Sep 17 00:00:00 2001
From: Luca <102960244+Luca-Timo@users.noreply.github.com>
Date: Sun, 31 May 2026 22:44:00 +0200
Subject: [PATCH] fix(restore-wizard): surface failure status, stop showing
"completed at 0%"
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
The progress step used a binary `isRunning ? "in progress" : "completed"`
check. So when the backend rejected the restore (pre-flight validator
threw, path error, etc.) the wizard cheerfully rendered "Restore
completed" with 0% progress and no error context — the admin had to
SSH into the server and inspect `restore_runs.error_message` to find
out what happened.
Now reads the most recent row from `restoreStatus.history[0]` and
renders one of three states:
- running → blue text, progress bar updates
- succeeded → green tick + post-restore actions (existing behaviour)
- failed → red banner with the first line of error_message, and
a callout if was_rollback_attempted is true so the
admin knows the destination is safe to retry on top of.
Net: the wizard now tells the truth about what just happened.
---
.../src/components/admin/RestoreWizard.jsx | 117 ++++++++++++++++--
1 file changed, 110 insertions(+), 7 deletions(-)
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 && (