feat: Backup & Restore - Manual backup tested complete.

This commit is contained in:
Eric Gullickson
2025-12-25 10:50:09 -06:00
parent 8ef6b3d853
commit 0357ce391f
38 changed files with 5734 additions and 1415447 deletions

View File

@@ -430,23 +430,6 @@ export const SettingsPage: React.FC = () => {
</ListItemSecondaryAction>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Station Management"
secondary="Manage gas station data and locations"
sx={{ pl: 7 }}
/>
<ListItemSecondaryAction>
<MuiButton
variant="outlined"
size="small"
onClick={() => navigate('/garage/settings/admin/stations')}
>
Manage
</MuiButton>
</ListItemSecondaryAction>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Email Templates"
@@ -463,6 +446,23 @@ export const SettingsPage: React.FC = () => {
</MuiButton>
</ListItemSecondaryAction>
</ListItem>
<Divider />
<ListItem>
<ListItemText
primary="Backup & Restore"
secondary="Create backups and restore data"
sx={{ pl: 7 }}
/>
<ListItemSecondaryAction>
<MuiButton
variant="outlined"
size="small"
onClick={() => navigate('/garage/settings/admin/backup')}
>
Manage
</MuiButton>
</ListItemSecondaryAction>
</ListItem>
</List>
</Card>
)}

View File

@@ -0,0 +1,921 @@
/**
* @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<TabPanelProps> = ({ children, value, index }) => {
return (
<div role="tabpanel" hidden={value !== index}>
{value === index && <Box sx={{ py: 3 }}>{children}</Box>}
</div>
);
};
export const AdminBackupPage: React.FC = () => {
const { loading: authLoading, isAdmin } = useAdminAccess();
const [tabValue, setTabValue] = useState(0);
const fileInputRef = useRef<HTMLInputElement>(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<BackupSchedule | null>(null);
const [selectedBackup, setSelectedBackup] = useState<BackupHistory | null>(null);
const [backupName, setBackupName] = useState('');
const [includeDocuments, setIncludeDocuments] = useState(true);
const [scheduleName, setScheduleName] = useState('');
const [scheduleFrequency, setScheduleFrequency] = useState<BackupFrequency>('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<HTMLInputElement>) => {
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 (
<Container maxWidth="lg" sx={{ py: 4 }}>
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
<CircularProgress />
</Box>
</Container>
);
}
// Not admin
if (!isAdmin) {
return <Navigate to="/garage/settings" replace />;
}
return (
<Container maxWidth="lg" sx={{ py: 4 }}>
<Box sx={{ mb: 4 }}>
<Typography variant="h4" component="h1" sx={{ mb: 1, fontWeight: 600 }}>
<Box display="flex" alignItems="center" gap={2}>
<Backup />
Backup & Restore
</Box>
</Typography>
<Typography variant="body1" color="text.secondary">
Manage database backups, schedules, and restore operations
</Typography>
</Box>
<Box sx={{ borderBottom: 1, borderColor: 'divider', mb: 2 }}>
<Tabs value={tabValue} onChange={(_, v) => setTabValue(v)}>
<Tab label="Backups" icon={<Backup />} iconPosition="start" />
<Tab label="Schedules" icon={<Schedule />} iconPosition="start" />
<Tab label="Settings" icon={<Settings />} iconPosition="start" />
</Tabs>
</Box>
{/* Backups Tab */}
<TabPanel value={tabValue} index={0}>
<Box sx={{ mb: 3, display: 'flex', gap: 2 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => setCreateBackupDialogOpen(true)}
>
Create Backup
</Button>
<Button
variant="outlined"
startIcon={<Upload />}
onClick={() => fileInputRef.current?.click()}
>
Upload Backup
</Button>
<input
ref={fileInputRef}
type="file"
accept=".tar.gz,.tar"
style={{ display: 'none' }}
onChange={handleUploadBackup}
/>
</Box>
{backupsLoading ? (
<Box display="flex" justifyContent="center" py={8}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Filename</TableCell>
<TableCell>Type</TableCell>
<TableCell>Size</TableCell>
<TableCell>Status</TableCell>
<TableCell>Created</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{backupsData?.items.map((backup) => (
<TableRow key={backup.id}>
<TableCell>{backup.filename}</TableCell>
<TableCell>
<Chip
label={backup.backupType}
size="small"
color={backup.backupType === 'scheduled' ? 'primary' : 'default'}
/>
</TableCell>
<TableCell>{formatFileSize(backup.fileSizeBytes)}</TableCell>
<TableCell>
<Chip
label={backup.status}
size="small"
color={
backup.status === 'completed'
? 'success'
: backup.status === 'failed'
? 'error'
: 'warning'
}
/>
</TableCell>
<TableCell>{formatDate(backup.startedAt)}</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => handleDownloadBackup(backup)}
title="Download"
>
<Download />
</IconButton>
<IconButton
size="small"
onClick={() => handleRestorePreview(backup)}
title="Restore"
disabled={backup.status !== 'completed'}
>
<RestorePage />
</IconButton>
<IconButton
size="small"
onClick={() => {
setSelectedBackup(backup);
setDeleteBackupDialogOpen(true);
}}
title="Delete"
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</TabPanel>
{/* Schedules Tab */}
<TabPanel value={tabValue} index={1}>
<Box sx={{ mb: 3 }}>
<Button
variant="contained"
startIcon={<Add />}
onClick={() => setCreateScheduleDialogOpen(true)}
>
Create Schedule
</Button>
</Box>
{schedulesLoading ? (
<Box display="flex" justifyContent="center" py={8}>
<CircularProgress />
</Box>
) : (
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Name</TableCell>
<TableCell>Frequency</TableCell>
<TableCell>Retention</TableCell>
<TableCell>Last Run</TableCell>
<TableCell>Next Run</TableCell>
<TableCell>Status</TableCell>
<TableCell align="right">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{schedules?.map((schedule) => (
<TableRow key={schedule.id}>
<TableCell>{schedule.name}</TableCell>
<TableCell>
<Chip label={schedule.frequency} size="small" />
</TableCell>
<TableCell>{schedule.retentionCount} backups</TableCell>
<TableCell>
{schedule.lastRunAt ? formatDate(schedule.lastRunAt) : 'Never'}
</TableCell>
<TableCell>
{schedule.nextRunAt ? formatDate(schedule.nextRunAt) : 'N/A'}
</TableCell>
<TableCell>
<Switch
checked={schedule.isEnabled}
onChange={() => handleToggleSchedule(schedule)}
color="primary"
/>
</TableCell>
<TableCell align="right">
<IconButton
size="small"
onClick={() => handleEditSchedule(schedule)}
title="Edit"
>
<Edit />
</IconButton>
<IconButton
size="small"
onClick={() => {
setSelectedSchedule(schedule);
setDeleteScheduleDialogOpen(true);
}}
title="Delete"
>
<Delete />
</IconButton>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
</TabPanel>
{/* Settings Tab */}
<TabPanel value={tabValue} index={2}>
{settingsLoading ? (
<Box display="flex" justifyContent="center" py={8}>
<CircularProgress />
</Box>
) : settings ? (
<Card>
<CardContent>
<Typography variant="h6" gutterBottom>
Backup Settings
</Typography>
<Box sx={{ mt: 3 }}>
<FormControlLabel
control={
<Switch
checked={settings.emailOnSuccess}
onChange={(e) => handleUpdateSettings('emailOnSuccess', e.target.checked)}
/>
}
label="Email on backup success"
/>
</Box>
<Box sx={{ mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.emailOnFailure}
onChange={(e) => handleUpdateSettings('emailOnFailure', e.target.checked)}
/>
}
label="Email on backup failure"
/>
</Box>
<Box sx={{ mt: 3 }}>
<TextField
label="Admin Email"
value={settings.adminEmail}
onChange={(e) => handleUpdateSettings('adminEmail', e.target.value)}
fullWidth
helperText="Email address to receive backup notifications"
/>
</Box>
<Box sx={{ mt: 3 }}>
<TextField
label="Max Backup Size (MB)"
type="number"
value={settings.maxBackupSizeMb}
onChange={(e) =>
handleUpdateSettings('maxBackupSizeMb', parseInt(e.target.value, 10))
}
fullWidth
helperText="Maximum backup file size in megabytes"
/>
</Box>
<Box sx={{ mt: 2 }}>
<FormControlLabel
control={
<Switch
checked={settings.compressionEnabled}
onChange={(e) => handleUpdateSettings('compressionEnabled', e.target.checked)}
/>
}
label="Enable compression"
/>
</Box>
</CardContent>
</Card>
) : null}
</TabPanel>
{/* Create Backup Dialog */}
<Dialog
open={createBackupDialogOpen}
onClose={() => setCreateBackupDialogOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Create Manual Backup</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
<TextField
label="Backup Name (optional)"
fullWidth
value={backupName}
onChange={(e) => setBackupName(e.target.value)}
placeholder="e.g., Pre-upgrade backup"
/>
<FormControlLabel
control={
<Switch
checked={includeDocuments}
onChange={(e) => setIncludeDocuments(e.target.checked)}
/>
}
label="Include document files"
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setCreateBackupDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleCreateBackup}
variant="contained"
disabled={createBackupMutation.isPending}
>
{createBackupMutation.isPending ? 'Creating...' : 'Create Backup'}
</Button>
</DialogActions>
</Dialog>
{/* Create Schedule Dialog */}
<Dialog
open={createScheduleDialogOpen}
onClose={() => setCreateScheduleDialogOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Create Backup Schedule</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
<TextField
label="Schedule Name"
fullWidth
value={scheduleName}
onChange={(e) => setScheduleName(e.target.value)}
required
/>
<FormControl fullWidth>
<InputLabel>Frequency</InputLabel>
<Select
value={scheduleFrequency}
onChange={(e) => setScheduleFrequency(e.target.value as BackupFrequency)}
label="Frequency"
>
<MenuItem value="hourly">Hourly</MenuItem>
<MenuItem value="daily">Daily</MenuItem>
<MenuItem value="weekly">Weekly</MenuItem>
<MenuItem value="monthly">Monthly</MenuItem>
</Select>
</FormControl>
<TextField
label="Retention Count"
type="number"
fullWidth
value={scheduleRetention}
onChange={(e) => setScheduleRetention(parseInt(e.target.value, 10))}
helperText="Number of backups to keep"
/>
<FormControlLabel
control={
<Switch
checked={scheduleEnabled}
onChange={(e) => setScheduleEnabled(e.target.checked)}
/>
}
label="Enabled"
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setCreateScheduleDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleCreateSchedule}
variant="contained"
disabled={!scheduleName || createScheduleMutation.isPending}
>
{createScheduleMutation.isPending ? 'Creating...' : 'Create Schedule'}
</Button>
</DialogActions>
</Dialog>
{/* Edit Schedule Dialog */}
<Dialog
open={editScheduleDialogOpen}
onClose={() => setEditScheduleDialogOpen(false)}
maxWidth="sm"
fullWidth
>
<DialogTitle>Edit Schedule</DialogTitle>
<DialogContent>
<Box sx={{ mt: 2, display: 'flex', flexDirection: 'column', gap: 3 }}>
<TextField
label="Schedule Name"
fullWidth
value={scheduleName}
onChange={(e) => setScheduleName(e.target.value)}
required
/>
<FormControl fullWidth>
<InputLabel>Frequency</InputLabel>
<Select
value={scheduleFrequency}
onChange={(e) => setScheduleFrequency(e.target.value as BackupFrequency)}
label="Frequency"
>
<MenuItem value="hourly">Hourly</MenuItem>
<MenuItem value="daily">Daily</MenuItem>
<MenuItem value="weekly">Weekly</MenuItem>
<MenuItem value="monthly">Monthly</MenuItem>
</Select>
</FormControl>
<TextField
label="Retention Count"
type="number"
fullWidth
value={scheduleRetention}
onChange={(e) => setScheduleRetention(parseInt(e.target.value, 10))}
helperText="Number of backups to keep"
/>
<FormControlLabel
control={
<Switch
checked={scheduleEnabled}
onChange={(e) => setScheduleEnabled(e.target.checked)}
/>
}
label="Enabled"
/>
</Box>
</DialogContent>
<DialogActions>
<Button onClick={() => setEditScheduleDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleUpdateSchedule}
variant="contained"
disabled={!scheduleName || updateScheduleMutation.isPending}
>
{updateScheduleMutation.isPending ? 'Saving...' : 'Save'}
</Button>
</DialogActions>
</Dialog>
{/* Delete Schedule Dialog */}
<Dialog open={deleteScheduleDialogOpen} onClose={() => setDeleteScheduleDialogOpen(false)}>
<DialogTitle>Delete Schedule</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete the schedule "{selectedSchedule?.name}"? This action
cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteScheduleDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteSchedule}
variant="contained"
color="error"
disabled={deleteScheduleMutation.isPending}
>
{deleteScheduleMutation.isPending ? 'Deleting...' : 'Delete'}
</Button>
</DialogActions>
</Dialog>
{/* Delete Backup Dialog */}
<Dialog open={deleteBackupDialogOpen} onClose={() => setDeleteBackupDialogOpen(false)}>
<DialogTitle>Delete Backup</DialogTitle>
<DialogContent>
<Typography>
Are you sure you want to delete the backup "{selectedBackup?.filename}"? This action
cannot be undone.
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setDeleteBackupDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleDeleteBackup}
variant="contained"
color="error"
disabled={deleteBackupMutation.isPending}
>
{deleteBackupMutation.isPending ? 'Deleting...' : 'Delete'}
</Button>
</DialogActions>
</Dialog>
{/* Restore Preview Dialog */}
<Dialog
open={restorePreviewDialogOpen}
onClose={() => setRestorePreviewDialogOpen(false)}
maxWidth="md"
fullWidth
>
<DialogTitle>Restore Preview</DialogTitle>
<DialogContent>
{restorePreviewMutation.isPending ? (
<Box display="flex" justifyContent="center" py={4}>
<CircularProgress />
</Box>
) : restorePreviewMutation.data ? (
<Box sx={{ mt: 2 }}>
<Alert severity="warning" sx={{ mb: 3 }}>
A safety backup will be created automatically before restoring.
</Alert>
<Typography variant="h6" gutterBottom>
Backup Information
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Version: {restorePreviewMutation.data.manifest.version}
</Typography>
<Typography variant="body2" color="text.secondary" gutterBottom>
Created: {formatDate(restorePreviewMutation.data.manifest.createdAt)}
</Typography>
<Divider sx={{ my: 2 }} />
<Typography variant="h6" gutterBottom>
Contents
</Typography>
<Typography variant="body2">
Database Tables: {restorePreviewMutation.data.manifest.contents.database.tablesCount}
</Typography>
<Typography variant="body2">
Database Size:{' '}
{formatFileSize(restorePreviewMutation.data.manifest.contents.database.sizeBytes)}
</Typography>
<Typography variant="body2">
Documents: {restorePreviewMutation.data.manifest.contents.documents.totalFiles}
</Typography>
<Typography variant="body2">
Documents Size:{' '}
{formatFileSize(
restorePreviewMutation.data.manifest.contents.documents.totalSizeBytes
)}
</Typography>
{restorePreviewMutation.data.warnings.length > 0 && (
<>
<Divider sx={{ my: 2 }} />
<Typography variant="h6" gutterBottom color="warning.main">
Warnings
</Typography>
{restorePreviewMutation.data.warnings.map((warning, index) => (
<Alert key={index} severity="warning" sx={{ mb: 1 }}>
{warning}
</Alert>
))}
</>
)}
<Divider sx={{ my: 2 }} />
<Typography variant="body2" color="text.secondary">
Estimated Duration: {restorePreviewMutation.data.estimatedDuration}
</Typography>
</Box>
) : null}
</DialogContent>
<DialogActions>
<Button onClick={() => setRestorePreviewDialogOpen(false)}>Cancel</Button>
<Button
onClick={() => {
setRestorePreviewDialogOpen(false);
setRestoreDialogOpen(true);
}}
variant="contained"
color="warning"
>
Proceed to Restore
</Button>
</DialogActions>
</Dialog>
{/* Restore Confirmation Dialog */}
<Dialog open={restoreDialogOpen} onClose={() => setRestoreDialogOpen(false)}>
<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.
</Alert>
<Typography>
Are you sure you want to restore from "{selectedBackup?.filename}"?
</Typography>
</DialogContent>
<DialogActions>
<Button onClick={() => setRestoreDialogOpen(false)}>Cancel</Button>
<Button
onClick={handleExecuteRestore}
variant="contained"
color="error"
disabled={executeRestoreMutation.isPending}
>
{executeRestoreMutation.isPending ? 'Restoring...' : 'Restore'}
</Button>
</DialogActions>
</Dialog>
</Container>
);
};

View File

@@ -1,53 +0,0 @@
/**
* @ai-summary Desktop admin page for gas station management
* @ai-context CRUD operations for global station data
*/
import React from 'react';
import { Navigate } from 'react-router-dom';
import { Box, Typography, CircularProgress } from '@mui/material';
import { Card } from '../../shared-minimal/components/Card';
import { useAdminAccess } from '../../core/auth/useAdminAccess';
export const AdminStationsPage: React.FC = () => {
const { isAdmin, loading } = useAdminAccess();
if (loading) {
return (
<Box sx={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '50vh' }}>
<CircularProgress />
</Box>
);
}
if (!isAdmin) {
return <Navigate to="/garage/settings" replace />;
}
return (
<Box sx={{ py: 2 }}>
<Typography variant="h4" sx={{ fontWeight: 700, color: 'text.primary', mb: 4 }}>
Station Management
</Typography>
<Card>
<Typography variant="h6" sx={{ fontWeight: 600, mb: 3 }}>
Gas Stations
</Typography>
<Typography variant="body2" color="text.secondary">
Station management interface coming soon.
</Typography>
<Typography variant="body2" color="text.secondary" sx={{ mt: 2 }}>
Features:
</Typography>
<ul>
<li>View all gas stations</li>
<li>Create new stations</li>
<li>Update station information</li>
<li>Delete stations</li>
<li>View station usage statistics</li>
</ul>
</Card>
</Box>
);
};