diff --git a/README.md b/README.md index e8cc1ed..86f1ea7 100644 --- a/README.md +++ b/README.md @@ -31,4 +31,5 @@ make migrate # run DB migrations - View active environment on production: `sudo cat /opt/motovaultpro/config/deployment/state.json` - Switch traffic between environments on production: `sudo ./scripts/ci/switch-traffic.sh blue instant` - View which container images are running: `docker ps --format 'table {{.Names}}\t{{.Image}}'` -- Flush all redis cache: `docker compose exec -T mvp-redis sh -lc "redis-cli FLUSHALL"` \ No newline at end of file +- Flush all redis cache: `docker compose exec -T mvp-redis sh -lc "redis-cli FLUSHALL"` +- Flush all backup data on staging before restoring: `docker compose exec mvp-postgres psql -U postgres -d motovaultpro -c "TRUNCATE TABLE backup_history, backup_schedules, backup_settings RESTART IDENTITY CASCADE;"` \ No newline at end of file diff --git a/backend/src/features/backup/domain/backup-restore.service.ts b/backend/src/features/backup/domain/backup-restore.service.ts index 391e5cb..c2c3b85 100644 --- a/backend/src/features/backup/domain/backup-restore.service.ts +++ b/backend/src/features/backup/domain/backup-restore.service.ts @@ -214,11 +214,9 @@ export class BackupRestoreService { const pgEnv = { ...process.env, PGPASSWORD: dbPassword }; try { - // Drop existing connections (except our own) - await execAsync( - `psql -h ${dbHost} -p ${dbPort} -U ${dbUser} -d postgres -c "SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '${dbName}' AND pid <> pg_backend_pid();"`, - { env: pgEnv } - ); + // Note: We no longer terminate connections before restore. + // The --clean flag in pg_dump generates DROP statements that handle existing data. + // Terminating connections would kill the backend's own pool and break the HTTP response. // Restore the database using psql await execAsync( diff --git a/frontend/src/features/admin/api/admin.api.ts b/frontend/src/features/admin/api/admin.api.ts index f68307a..0dfd2e0 100644 --- a/frontend/src/features/admin/api/admin.api.ts +++ b/frontend/src/features/admin/api/admin.api.ts @@ -48,6 +48,7 @@ import { CreateScheduleRequest, UpdateScheduleRequest, RestorePreviewResponse, + ExecuteRestoreRequest, } from '../types/admin.types'; export interface AuditLogsResponse { @@ -408,9 +409,10 @@ export const adminApi = { }, // Execute restore - restore: async (id: string): Promise<{ message: string }> => { + restore: async (id: string, options?: ExecuteRestoreRequest): Promise<{ message: string }> => { const response = await apiClient.post<{ message: string }>( - `/admin/backups/${id}/restore` + `/admin/backups/${id}/restore`, + options ); return response.data; }, diff --git a/frontend/src/features/admin/hooks/useBackups.ts b/frontend/src/features/admin/hooks/useBackups.ts index 87278d0..e326974 100644 --- a/frontend/src/features/admin/hooks/useBackups.ts +++ b/frontend/src/features/admin/hooks/useBackups.ts @@ -10,6 +10,7 @@ import { CreateBackupRequest, CreateScheduleRequest, UpdateScheduleRequest, + ExecuteRestoreRequest, } from '../types/admin.types'; import toast from 'react-hot-toast'; @@ -144,7 +145,8 @@ export const useExecuteRestore = () => { const queryClient = useQueryClient(); return useMutation({ - mutationFn: (id: string) => adminApi.backups.restore(id), + mutationFn: ({ id, options }: { id: string; options?: ExecuteRestoreRequest }) => + adminApi.backups.restore(id, options), onSuccess: (data) => { queryClient.invalidateQueries({ queryKey: backupKeys.all }); toast.success(data.message || 'Restore completed successfully'); diff --git a/frontend/src/features/admin/mobile/AdminBackupMobileScreen.tsx b/frontend/src/features/admin/mobile/AdminBackupMobileScreen.tsx index fded738..87221dd 100644 --- a/frontend/src/features/admin/mobile/AdminBackupMobileScreen.tsx +++ b/frontend/src/features/admin/mobile/AdminBackupMobileScreen.tsx @@ -69,6 +69,7 @@ export const AdminBackupMobileScreen: React.FC = () => { const [showDeleteBackupConfirm, setShowDeleteBackupConfirm] = useState(false); const [showDeleteScheduleConfirm, setShowDeleteScheduleConfirm] = useState(false); const [showRestoreConfirm, setShowRestoreConfirm] = useState(false); + const [createSafetyBackup, setCreateSafetyBackup] = useState(true); // Queries const { data: backupsData, isLoading: backupsLoading } = useBackups(); @@ -152,14 +153,18 @@ export const AdminBackupMobileScreen: React.FC = () => { const handleExecuteRestore = useCallback(() => { if (!selectedBackup) return; - executeRestoreMutation.mutate(selectedBackup.id, { - onSuccess: () => { - setShowRestoreConfirm(false); - setViewMode('list'); - setSelectedBackup(null); - }, - }); - }, [selectedBackup, executeRestoreMutation]); + executeRestoreMutation.mutate( + { id: selectedBackup.id, options: { createSafetyBackup } }, + { + onSuccess: () => { + setShowRestoreConfirm(false); + setViewMode('list'); + setSelectedBackup(null); + setCreateSafetyBackup(true); + }, + } + ); + }, [selectedBackup, createSafetyBackup, executeRestoreMutation]); // Handlers for schedules const handleCreateSchedule = useCallback(() => { @@ -543,11 +548,33 @@ export const AdminBackupMobileScreen: React.FC = () => { -
- A safety backup will be created automatically before restoring. -
-+ Warning: Without a safety backup, you cannot undo this restore operation. +
+This action will replace all current data with the backup data. + {createSafetyBackup + ? ' A safety backup will be created first.' + : ' No safety backup will be created - this cannot be undone!'}
diff --git a/frontend/src/features/admin/types/admin.types.ts b/frontend/src/features/admin/types/admin.types.ts
index becbeb0..d3a183d 100644
--- a/frontend/src/features/admin/types/admin.types.ts
+++ b/frontend/src/features/admin/types/admin.types.ts
@@ -367,3 +367,7 @@ export interface RestorePreviewResponse {
warnings: string[];
estimatedDuration: string;
}
+
+export interface ExecuteRestoreRequest {
+ createSafetyBackup?: boolean;
+}
diff --git a/frontend/src/pages/admin/AdminBackupPage.tsx b/frontend/src/pages/admin/AdminBackupPage.tsx
index 7c1aabb..c82a4ff 100644
--- a/frontend/src/pages/admin/AdminBackupPage.tsx
+++ b/frontend/src/pages/admin/AdminBackupPage.tsx
@@ -127,6 +127,7 @@ export const AdminBackupPage: React.FC = () => {
const [scheduleFrequency, setScheduleFrequency] = useState