/** * @ai-summary Admin Backup & Restore page for managing backups, schedules, and settings * @ai-context Desktop version with tabbed interface for backup management */ import React, { useState, useCallback, useRef } from 'react'; import { Navigate } from 'react-router-dom'; import dayjs from 'dayjs'; import { Box, Button, Card, CardContent, Chip, CircularProgress, Container, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Switch, Tab, Tabs, TextField, Typography, Alert, Divider, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, IconButton, MenuItem, Select, FormControl, InputLabel, Paper, } from '@mui/material'; import { Backup, Download, Delete, Schedule, Settings, Add, Upload, RestorePage, Edit, } from '@mui/icons-material'; import { useAdminAccess } from '../../core/auth/useAdminAccess'; import { useBackups, useBackupSchedules, useBackupSettings, useCreateBackup, useDeleteBackup, useDownloadBackup, useUploadBackup, useRestorePreview, useExecuteRestore, useCreateSchedule, useUpdateSchedule, useDeleteSchedule, useToggleSchedule, useUpdateSettings, } from '../../features/admin/hooks/useBackups'; import { BackupSchedule, BackupSettings, CreateScheduleRequest, UpdateScheduleRequest, BackupFrequency, BackupHistory, } from '../../features/admin/types/admin.types'; // Helper to format file size const formatFileSize = (bytes: number): string => { if (bytes === 0) return '0 B'; const k = 1024; const sizes = ['B', 'KB', 'MB', 'GB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return Math.round((bytes / Math.pow(k, i)) * 100) / 100 + ' ' + sizes[i]; }; // Helper to format date const formatDate = (dateString: string): string => { return dayjs(dateString).format('MMM DD, YYYY HH:mm'); }; interface TabPanelProps { children?: React.ReactNode; index: number; value: number; } const TabPanel: React.FC = ({ children, value, index }) => { return ( ); }; export const AdminBackupPage: React.FC = () => { const { loading: authLoading, isAdmin } = useAdminAccess(); const [tabValue, setTabValue] = useState(0); const fileInputRef = useRef(null); // State for dialogs const [createBackupDialogOpen, setCreateBackupDialogOpen] = useState(false); const [createScheduleDialogOpen, setCreateScheduleDialogOpen] = useState(false); const [editScheduleDialogOpen, setEditScheduleDialogOpen] = useState(false); const [deleteScheduleDialogOpen, setDeleteScheduleDialogOpen] = useState(false); const [deleteBackupDialogOpen, setDeleteBackupDialogOpen] = useState(false); const [restoreDialogOpen, setRestoreDialogOpen] = useState(false); const [restorePreviewDialogOpen, setRestorePreviewDialogOpen] = useState(false); // State for forms const [selectedSchedule, setSelectedSchedule] = useState(null); const [selectedBackup, setSelectedBackup] = useState(null); const [backupName, setBackupName] = useState(''); const [includeDocuments, setIncludeDocuments] = useState(true); const [scheduleName, setScheduleName] = useState(''); const [scheduleFrequency, setScheduleFrequency] = useState('daily'); const [scheduleRetention, setScheduleRetention] = useState(7); const [scheduleEnabled, setScheduleEnabled] = useState(true); // Queries const { data: backupsData, isLoading: backupsLoading } = useBackups(); const { data: schedules, isLoading: schedulesLoading } = useBackupSchedules(); const { data: settings, isLoading: settingsLoading } = useBackupSettings(); // Mutations const createBackupMutation = useCreateBackup(); const deleteBackupMutation = useDeleteBackup(); const downloadBackupMutation = useDownloadBackup(); const uploadBackupMutation = useUploadBackup(); const restorePreviewMutation = useRestorePreview(); const executeRestoreMutation = useExecuteRestore(); const createScheduleMutation = useCreateSchedule(); const updateScheduleMutation = useUpdateSchedule(); const deleteScheduleMutation = useDeleteSchedule(); const toggleScheduleMutation = useToggleSchedule(); const updateSettingsMutation = useUpdateSettings(); // Handlers for backups const handleCreateBackup = useCallback(() => { createBackupMutation.mutate( { name: backupName || undefined, includeDocuments, }, { onSuccess: () => { setCreateBackupDialogOpen(false); setBackupName(''); setIncludeDocuments(true); }, } ); }, [backupName, includeDocuments, createBackupMutation]); const handleDownloadBackup = useCallback( (backup: BackupHistory) => { downloadBackupMutation.mutate({ id: backup.id, filename: backup.filename, }); }, [downloadBackupMutation] ); const handleDeleteBackup = useCallback(() => { if (!selectedBackup) return; deleteBackupMutation.mutate(selectedBackup.id, { onSuccess: () => { setDeleteBackupDialogOpen(false); setSelectedBackup(null); }, }); }, [selectedBackup, deleteBackupMutation]); const handleUploadBackup = useCallback( (event: React.ChangeEvent) => { const file = event.target.files?.[0]; if (file) { uploadBackupMutation.mutate(file); } if (fileInputRef.current) { fileInputRef.current.value = ''; } }, [uploadBackupMutation] ); const handleRestorePreview = useCallback( (backup: BackupHistory) => { setSelectedBackup(backup); restorePreviewMutation.mutate(backup.id, { onSuccess: () => { setRestorePreviewDialogOpen(true); }, }); }, [restorePreviewMutation] ); const handleExecuteRestore = useCallback(() => { if (!selectedBackup) return; executeRestoreMutation.mutate(selectedBackup.id, { onSuccess: () => { setRestoreDialogOpen(false); setRestorePreviewDialogOpen(false); setSelectedBackup(null); }, }); }, [selectedBackup, executeRestoreMutation]); // Handlers for schedules const handleCreateSchedule = useCallback(() => { const data: CreateScheduleRequest = { name: scheduleName, frequency: scheduleFrequency, retentionCount: scheduleRetention, isEnabled: scheduleEnabled, }; createScheduleMutation.mutate(data, { onSuccess: () => { setCreateScheduleDialogOpen(false); setScheduleName(''); setScheduleFrequency('daily'); setScheduleRetention(7); setScheduleEnabled(true); }, }); }, [scheduleName, scheduleFrequency, scheduleRetention, scheduleEnabled, createScheduleMutation]); const handleEditSchedule = useCallback( (schedule: BackupSchedule) => { setSelectedSchedule(schedule); setScheduleName(schedule.name); setScheduleFrequency(schedule.frequency); setScheduleRetention(schedule.retentionCount); setScheduleEnabled(schedule.isEnabled); setEditScheduleDialogOpen(true); }, [] ); const handleUpdateSchedule = useCallback(() => { if (!selectedSchedule) return; const data: UpdateScheduleRequest = { name: scheduleName !== selectedSchedule.name ? scheduleName : undefined, frequency: scheduleFrequency !== selectedSchedule.frequency ? scheduleFrequency : undefined, retentionCount: scheduleRetention !== selectedSchedule.retentionCount ? scheduleRetention : undefined, isEnabled: scheduleEnabled !== selectedSchedule.isEnabled ? scheduleEnabled : undefined, }; updateScheduleMutation.mutate( { id: selectedSchedule.id, data }, { onSuccess: () => { setEditScheduleDialogOpen(false); setSelectedSchedule(null); }, } ); }, [ selectedSchedule, scheduleName, scheduleFrequency, scheduleRetention, scheduleEnabled, updateScheduleMutation, ]); const handleDeleteSchedule = useCallback(() => { if (!selectedSchedule) return; deleteScheduleMutation.mutate(selectedSchedule.id, { onSuccess: () => { setDeleteScheduleDialogOpen(false); setSelectedSchedule(null); }, }); }, [selectedSchedule, deleteScheduleMutation]); const handleToggleSchedule = useCallback( (schedule: BackupSchedule) => { toggleScheduleMutation.mutate(schedule.id); }, [toggleScheduleMutation] ); // Handlers for settings const handleUpdateSettings = useCallback( (field: keyof BackupSettings, value: boolean | string | number) => { if (!settings) return; updateSettingsMutation.mutate({ [field]: value }); }, [settings, updateSettingsMutation] ); // Auth loading if (authLoading) { return ( ); } // Not admin if (!isAdmin) { return ; } return ( Backup & Restore Manage database backups, schedules, and restore operations setTabValue(v)}> } iconPosition="start" /> } iconPosition="start" /> } iconPosition="start" /> {/* Backups Tab */} {backupsLoading ? ( ) : ( Filename Type Size Status Created Actions {backupsData?.items.map((backup) => ( {backup.filename} {formatFileSize(backup.fileSizeBytes)} {formatDate(backup.startedAt)} handleDownloadBackup(backup)} title="Download" > handleRestorePreview(backup)} title="Restore" disabled={backup.status !== 'completed'} > { setSelectedBackup(backup); setDeleteBackupDialogOpen(true); }} title="Delete" > ))}
)}
{/* Schedules Tab */} {schedulesLoading ? ( ) : ( Name Frequency Retention Last Run Next Run Status Actions {schedules?.map((schedule) => ( {schedule.name} {schedule.retentionCount} backups {schedule.lastRunAt ? formatDate(schedule.lastRunAt) : 'Never'} {schedule.nextRunAt ? formatDate(schedule.nextRunAt) : 'N/A'} handleToggleSchedule(schedule)} color="primary" /> handleEditSchedule(schedule)} title="Edit" > { setSelectedSchedule(schedule); setDeleteScheduleDialogOpen(true); }} title="Delete" > ))}
)}
{/* Settings Tab */} {settingsLoading ? ( ) : settings ? ( Backup Settings handleUpdateSettings('emailOnSuccess', e.target.checked)} /> } label="Email on backup success" /> handleUpdateSettings('emailOnFailure', e.target.checked)} /> } label="Email on backup failure" /> handleUpdateSettings('adminEmail', e.target.value)} fullWidth helperText="Email address to receive backup notifications" /> handleUpdateSettings('maxBackupSizeMb', parseInt(e.target.value, 10)) } fullWidth helperText="Maximum backup file size in megabytes" /> handleUpdateSettings('compressionEnabled', e.target.checked)} /> } label="Enable compression" /> ) : null} {/* Create Backup Dialog */} setCreateBackupDialogOpen(false)} maxWidth="sm" fullWidth > Create Manual Backup setBackupName(e.target.value)} placeholder="e.g., Pre-upgrade backup" /> setIncludeDocuments(e.target.checked)} /> } label="Include document files" /> {/* Create Schedule Dialog */} setCreateScheduleDialogOpen(false)} maxWidth="sm" fullWidth > Create Backup Schedule setScheduleName(e.target.value)} required /> Frequency setScheduleRetention(parseInt(e.target.value, 10))} helperText="Number of backups to keep" /> setScheduleEnabled(e.target.checked)} /> } label="Enabled" /> {/* Edit Schedule Dialog */} setEditScheduleDialogOpen(false)} maxWidth="sm" fullWidth > Edit Schedule setScheduleName(e.target.value)} required /> Frequency setScheduleRetention(parseInt(e.target.value, 10))} helperText="Number of backups to keep" /> setScheduleEnabled(e.target.checked)} /> } label="Enabled" /> {/* Delete Schedule Dialog */} setDeleteScheduleDialogOpen(false)}> Delete Schedule Are you sure you want to delete the schedule "{selectedSchedule?.name}"? This action cannot be undone. {/* Delete Backup Dialog */} setDeleteBackupDialogOpen(false)}> Delete Backup Are you sure you want to delete the backup "{selectedBackup?.filename}"? This action cannot be undone. {/* Restore Preview Dialog */} setRestorePreviewDialogOpen(false)} maxWidth="md" fullWidth > Restore Preview {restorePreviewMutation.isPending ? ( ) : restorePreviewMutation.data ? ( A safety backup will be created automatically before restoring. Backup Information Version: {restorePreviewMutation.data.manifest.version} Created: {formatDate(restorePreviewMutation.data.manifest.createdAt)} Contents Database Tables: {restorePreviewMutation.data.manifest.contents.database.tablesCount} Database Size:{' '} {formatFileSize(restorePreviewMutation.data.manifest.contents.database.sizeBytes)} Documents: {restorePreviewMutation.data.manifest.contents.documents.totalFiles} Documents Size:{' '} {formatFileSize( restorePreviewMutation.data.manifest.contents.documents.totalSizeBytes )} {restorePreviewMutation.data.warnings.length > 0 && ( <> Warnings {restorePreviewMutation.data.warnings.map((warning, index) => ( {warning} ))} )} Estimated Duration: {restorePreviewMutation.data.estimatedDuration} ) : null} {/* Restore Confirmation Dialog */} setRestoreDialogOpen(false)}> Confirm Restore This action will replace all current data with the backup data. A safety backup will be created first. Are you sure you want to restore from "{selectedBackup?.filename}"?
); };