feat: backup improvements
All checks were successful
Deploy to Staging / Build Images (push) Successful in 4m31s
Deploy to Staging / Deploy to Staging (push) Successful in 37s
Deploy to Staging / Verify Staging (push) Successful in 6s
Deploy to Staging / Notify Staging Ready (push) Successful in 6s
Deploy to Staging / Notify Staging Failure (push) Has been skipped

This commit is contained in:
Eric Gullickson
2026-01-01 13:57:36 -06:00
parent ea482b745e
commit 9043a581b1
7 changed files with 92 additions and 35 deletions

View File

@@ -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;
},

View File

@@ -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');

View File

@@ -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 = () => {
</button>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
<p className="text-xs text-yellow-800">
A safety backup will be created automatically before restoring.
</p>
</div>
<GlassCard>
<div className="p-4">
<div className="flex items-center justify-between">
<span className="text-sm font-medium text-slate-700">Create safety backup</span>
<button
onClick={() => setCreateSafetyBackup(!createSafetyBackup)}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
createSafetyBackup ? 'bg-blue-600' : 'bg-gray-200'
}`}
style={{ minHeight: '44px', minWidth: '44px' }}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
createSafetyBackup ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{!createSafetyBackup && (
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-2 mt-2">
<p className="text-xs text-yellow-800">
Warning: Without a safety backup, you cannot undo this restore operation.
</p>
</div>
)}
</div>
</GlassCard>
<GlassCard>
<div className="p-4 space-y-3">
@@ -615,6 +642,9 @@ export const AdminBackupMobileScreen: React.FC = () => {
<div className="bg-red-50 border border-red-200 rounded-lg p-3 mb-4">
<p className="text-xs text-red-800">
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!'}
</p>
</div>
<p className="text-sm text-slate-600 mb-4">

View File

@@ -367,3 +367,7 @@ export interface RestorePreviewResponse {
warnings: string[];
estimatedDuration: string;
}
export interface ExecuteRestoreRequest {
createSafetyBackup?: boolean;
}

View File

@@ -127,6 +127,7 @@ export const AdminBackupPage: React.FC = () => {
const [scheduleFrequency, setScheduleFrequency] = useState<BackupFrequency>('daily');
const [scheduleRetention, setScheduleRetention] = useState(7);
const [scheduleEnabled, setScheduleEnabled] = useState(true);
const [createSafetyBackup, setCreateSafetyBackup] = useState(true);
// Queries
const { data: backupsData, isLoading: backupsLoading } = useBackups();
@@ -210,14 +211,18 @@ export const AdminBackupPage: React.FC = () => {
const handleExecuteRestore = useCallback(() => {
if (!selectedBackup) return;
executeRestoreMutation.mutate(selectedBackup.id, {
onSuccess: () => {
setRestoreDialogOpen(false);
setRestorePreviewDialogOpen(false);
setSelectedBackup(null);
},
});
}, [selectedBackup, executeRestoreMutation]);
executeRestoreMutation.mutate(
{ id: selectedBackup.id, options: { createSafetyBackup } },
{
onSuccess: () => {
setRestoreDialogOpen(false);
setRestorePreviewDialogOpen(false);
setSelectedBackup(null);
setCreateSafetyBackup(true);
},
}
);
}, [selectedBackup, createSafetyBackup, executeRestoreMutation]);
// Handlers for schedules
const handleCreateSchedule = useCallback(() => {
@@ -820,9 +825,22 @@ export const AdminBackupPage: React.FC = () => {
</Box>
) : restorePreviewMutation.data ? (
<Box sx={{ mt: 2 }}>
<Alert severity="warning" sx={{ mb: 3 }}>
A safety backup will be created automatically before restoring.
</Alert>
<Box sx={{ mb: 3 }}>
<FormControlLabel
control={
<Switch
checked={createSafetyBackup}
onChange={(e) => setCreateSafetyBackup(e.target.checked)}
/>
}
label="Create safety backup before restoring"
/>
{!createSafetyBackup && (
<Alert severity="warning" sx={{ mt: 1 }}>
Warning: Without a safety backup, you cannot undo this restore operation.
</Alert>
)}
</Box>
<Typography variant="h6" gutterBottom>
Backup Information
@@ -897,8 +915,10 @@ export const AdminBackupPage: React.FC = () => {
<DialogTitle>Confirm Restore</DialogTitle>
<DialogContent>
<Alert severity="error" sx={{ mb: 2 }}>
This action will replace all current data with the backup data. A safety backup will be
created first.
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!'}
</Alert>
<Typography>
Are you sure you want to restore from "{selectedBackup?.filename}"?