feat: service mode (maintenance mode)
Admins can toggle maintenance mode from the admin dashboard (new "Wartung" tab). When active, all non-admin users see a full-page maintenance screen instead of the app. - Backend: GET /api/config/service-mode endpoint (authenticated) - Backend: stores state in app_settings key 'service_mode' - Frontend: ServiceModeGuard wraps all ProtectedRoutes - Frontend: ServiceModePage full-screen maintenance UI - Frontend: ServiceModeTab in admin dashboard with toggle + message - Admins (dashboard_admin group) always bypass the guard Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
103
frontend/src/components/admin/ServiceModeTab.tsx
Normal file
103
frontend/src/components/admin/ServiceModeTab.tsx
Normal file
@@ -0,0 +1,103 @@
|
||||
import { useState } from 'react';
|
||||
import {
|
||||
Box, Card, CardContent, Typography, Switch, FormControlLabel,
|
||||
TextField, Button, Alert, CircularProgress,
|
||||
} from '@mui/material';
|
||||
import BuildIcon from '@mui/icons-material/Build';
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { settingsApi } from '../../services/settings';
|
||||
import { useNotification } from '../../contexts/NotificationContext';
|
||||
|
||||
export default function ServiceModeTab() {
|
||||
const queryClient = useQueryClient();
|
||||
const { showSuccess, showError } = useNotification();
|
||||
|
||||
const { data: setting, isLoading } = useQuery({
|
||||
queryKey: ['admin', 'settings', 'service_mode'],
|
||||
queryFn: () => settingsApi.get('service_mode'),
|
||||
});
|
||||
|
||||
const currentValue = setting?.value ?? { active: false, message: '' };
|
||||
const [active, setActive] = useState<boolean>(currentValue.active ?? false);
|
||||
const [message, setMessage] = useState<string>(currentValue.message ?? '');
|
||||
|
||||
// Sync state when data loads
|
||||
useState(() => {
|
||||
if (setting?.value) {
|
||||
setActive(setting.value.active ?? false);
|
||||
setMessage(setting.value.message ?? '');
|
||||
}
|
||||
});
|
||||
|
||||
const mutation = useMutation({
|
||||
mutationFn: (value: { active: boolean; message: string }) =>
|
||||
settingsApi.update('service_mode', value),
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({ queryKey: ['service-mode'] });
|
||||
queryClient.invalidateQueries({ queryKey: ['admin', 'settings', 'service_mode'] });
|
||||
showSuccess(active ? 'Wartungsmodus aktiviert' : 'Wartungsmodus deaktiviert');
|
||||
},
|
||||
onError: () => showError('Einstellung konnte nicht gespeichert werden'),
|
||||
});
|
||||
|
||||
const handleSave = () => {
|
||||
mutation.mutate({ active, message });
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return <Box sx={{ display: 'flex', justifyContent: 'center', p: 4 }}><CircularProgress /></Box>;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box sx={{ maxWidth: 600 }}>
|
||||
<Card>
|
||||
<CardContent>
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, mb: 3 }}>
|
||||
<BuildIcon color={active ? 'error' : 'action'} />
|
||||
<Typography variant="h6">Wartungsmodus</Typography>
|
||||
</Box>
|
||||
|
||||
{active && (
|
||||
<Alert severity="warning" sx={{ mb: 2 }}>
|
||||
Wartungsmodus ist aktiv. Normale Benutzer sehen die Wartungsseite.
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<FormControlLabel
|
||||
control={
|
||||
<Switch
|
||||
checked={active}
|
||||
onChange={(e) => setActive(e.target.checked)}
|
||||
color="error"
|
||||
/>
|
||||
}
|
||||
label={active ? 'Wartungsmodus aktiviert' : 'Wartungsmodus deaktiviert'}
|
||||
sx={{ mb: 3, display: 'block' }}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
fullWidth
|
||||
multiline
|
||||
rows={3}
|
||||
label="Nachricht für Benutzer"
|
||||
placeholder="Das Dashboard befindet sich aktuell im Wartungsmodus..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
sx={{ mb: 3 }}
|
||||
helperText="Diese Nachricht sehen Benutzer auf der Wartungsseite."
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="contained"
|
||||
color={active ? 'error' : 'primary'}
|
||||
onClick={handleSave}
|
||||
disabled={mutation.isPending}
|
||||
startIcon={mutation.isPending ? <CircularProgress size={16} /> : <BuildIcon />}
|
||||
>
|
||||
{mutation.isPending ? 'Wird gespeichert...' : 'Speichern'}
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user