Monitoring UX: Log leeren + PageSize wählbar

- Backend: DELETE /api/monitoring/events (settings:update). Optional
  ?olderThanDays=N – nur Events älter als N Tage löschen.
  Hinterlässt selbst einen Audit-Eintrag "Log geleert: X Einträge"
  mit User-E-Mail + IP, damit der Vorgang nachvollziehbar bleibt.
- Frontend: "Log leeren"-Button öffnet Bestätigungs-Modal mit
  optionalem "älter als X Tage"-Filter. Roter Bestätigungs-Button.
- Frontend: PageSize-Selector (10/25/50/100/200) neben dem Header.
  Wechsel setzt automatisch zurück auf Seite 1.

Live-verifiziert: Clear löscht 10 Events, schreibt 1 Audit-Event,
PageSize=5 wird in pagination respektiert.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-01 09:31:53 +02:00
parent 57eb027d5f
commit b5ba47aa93
4 changed files with 133 additions and 4 deletions
+82 -4
View File
@@ -7,7 +7,8 @@ import Card from '../../components/ui/Card';
import Button from '../../components/ui/Button';
import Input from '../../components/ui/Input';
import Select from '../../components/ui/Select';
import { ArrowLeft, Send, RefreshCw, Mail, ShieldAlert, ChevronLeft, ChevronRight } from 'lucide-react';
import Modal from '../../components/ui/Modal';
import { ArrowLeft, Send, RefreshCw, Mail, ShieldAlert, ChevronLeft, ChevronRight, Trash2 } from 'lucide-react';
const TYPE_OPTIONS: { value: SecurityEventType | ''; label: string }[] = [
{ value: '', label: 'Alle Typen' },
@@ -58,12 +59,15 @@ export default function Monitoring() {
const queryClient = useQueryClient();
const [page, setPage] = useState(1);
const [pageSize, setPageSize] = useState(50);
const [filters, setFilters] = useState({
type: '' as SecurityEventType | '',
severity: '' as SecuritySeverity | '',
search: '',
ip: '',
});
const [showClearConfirm, setShowClearConfirm] = useState(false);
const [clearOlderThanDays, setClearOlderThanDays] = useState<number | ''>('');
const [alertEmail, setAlertEmail] = useState('');
const [digestEnabled, setDigestEnabled] = useState(false);
@@ -82,11 +86,22 @@ export default function Monitoring() {
// Events laden
const { data: eventsData, isLoading: eventsLoading } = useQuery({
queryKey: ['monitoring-events', page, filters],
queryFn: () => monitoringApi.getEvents({ page, limit: 50, ...filters }),
queryKey: ['monitoring-events', page, pageSize, filters],
queryFn: () => monitoringApi.getEvents({ page, limit: pageSize, ...filters }),
refetchInterval: 30_000, // alle 30s neu laden
});
const clearEvents = useMutation({
mutationFn: (olderThanDays?: number) => monitoringApi.clearEvents(olderThanDays),
onSuccess: (res) => {
toast.success(res.message || 'Events gelöscht');
setShowClearConfirm(false);
setClearOlderThanDays('');
queryClient.invalidateQueries({ queryKey: ['monitoring-events'] });
},
onError: (e: Error) => toast.error(e.message || 'Löschen fehlgeschlagen'),
});
const saveSettings = useMutation({
mutationFn: () => monitoringApi.updateSettings({ alertEmail, digestEnabled }),
onSuccess: () => {
@@ -227,7 +242,26 @@ export default function Monitoring() {
{/* Tabelle */}
<Card>
<h2 className="text-lg font-semibold mb-3">Events</h2>
<div className="flex items-center justify-between mb-3 flex-wrap gap-2">
<h2 className="text-lg font-semibold">Events</h2>
<div className="flex items-center gap-2">
<label className="text-sm text-gray-600">Pro Seite:</label>
<select
value={pageSize}
onChange={(e) => { setPageSize(parseInt(e.target.value)); setPage(1); }}
className="px-2 py-1 border border-gray-300 rounded text-sm"
>
<option value={10}>10</option>
<option value={25}>25</option>
<option value={50}>50</option>
<option value={100}>100</option>
<option value={200}>200</option>
</select>
<Button variant="secondary" size="sm" onClick={() => setShowClearConfirm(true)}>
<Trash2 className="w-4 h-4 mr-1" /> Log leeren
</Button>
</div>
</div>
{eventsLoading ? (
<div className="text-gray-500 py-4">Lade</div>
) : events.length === 0 ? (
@@ -284,6 +318,50 @@ export default function Monitoring() {
</div>
)}
</Card>
{/* Clear-Confirm-Modal */}
<Modal
isOpen={showClearConfirm}
onClose={() => setShowClearConfirm(false)}
title="Security-Log leeren"
size="sm"
>
<div className="space-y-4">
<p className="text-sm text-gray-700">
Sicher? Alle Events werden aus der Datenbank entfernt. Ein
Audit-Log-Eintrag mit deinem Namen bleibt erhalten.
</p>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Nur Events älter als (Tage)
</label>
<input
type="number"
min="0"
value={clearOlderThanDays}
onChange={(e) => setClearOlderThanDays(e.target.value === '' ? '' : parseInt(e.target.value))}
placeholder="leer = alle löschen"
className="block w-full max-w-[200px] px-3 py-2 border border-gray-300 rounded text-sm"
/>
<p className="text-xs text-gray-500 mt-1">
Beispiel: 30 = nur Events älter als 30 Tage löschen.
</p>
</div>
<div className="flex justify-end gap-2 pt-2">
<Button variant="secondary" onClick={() => setShowClearConfirm(false)}>
Abbrechen
</Button>
<Button
onClick={() => clearEvents.mutate(clearOlderThanDays === '' ? undefined : Number(clearOlderThanDays))}
disabled={clearEvents.isPending}
className="!bg-red-600 hover:!bg-red-700"
>
<Trash2 className="w-4 h-4 mr-1" />
{clearOlderThanDays === '' ? 'Alle löschen' : `Älter als ${clearOlderThanDays} Tage löschen`}
</Button>
</div>
</div>
</Modal>
</div>
);
}
+5
View File
@@ -1489,6 +1489,11 @@ export const monitoringApi = {
const res = await api.post<ApiResponse<{ sent: boolean; eventCount: number; reason?: string }>>('/monitoring/run-digest');
return res.data;
},
clearEvents: async (olderThanDays?: number) => {
const q = olderThanDays ? `?olderThanDays=${olderThanDays}` : '';
const res = await api.delete<ApiResponse<{ deletedCount: number }>>(`/monitoring/events${q}`);
return res.data;
},
};
export const emailLogApi = {