fix: dashboard layout, widget caching, and backend stability
Layout: - Remove Container maxWidth cap so widgets scale fluidly on wide screens - Fix ActivityFeed Card missing height:100% and overflow:hidden that caused the timeline connector pseudo-element to bleed outside the card boundary Performance (frontend): - Migrate VehicleDashboardCard, EquipmentDashboardCard, AtemschutzDashboardCard, UpcomingEventsWidget, and PersonalWarningsBanner from useEffect+useState to TanStack Query — cached for 5 min, so navigating back to the dashboard no longer re-fires all 9 API requests - Add gcTime:10min and refetchOnWindowFocus:false to QueryClient defaults to prevent spurious refetches on tab-switch Backend stability: - Raise default RATE_LIMIT_MAX from 100 to 300 req/15min — the previous limit was easily exceeded by a single active user during normal dashboard navigation - Increase DB connectionTimeoutMillis from 2s to 5s to handle burst-load scenarios where multiple requests compete for pool slots simultaneously Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ const ActivityFeed: React.FC = () => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Card sx={{ height: '100%', overflow: 'hidden' }}>
|
||||
<CardContent>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Letzte Aktivitäten
|
||||
@@ -110,7 +110,7 @@ const ActivityFeed: React.FC = () => {
|
||||
position: 'absolute',
|
||||
left: 19,
|
||||
top: 56,
|
||||
bottom: -8,
|
||||
bottom: 0,
|
||||
width: 2,
|
||||
bgcolor: 'divider',
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Alert,
|
||||
AlertTitle,
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { NotificationsActive as NotificationsActiveIcon } from '@mui/icons-material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { atemschutzApi } from '../../services/atemschutz';
|
||||
import type { User } from '../../types/auth.types';
|
||||
import type { AtemschutzUebersicht } from '../../types/atemschutz.types';
|
||||
@@ -74,33 +75,14 @@ interface PersonalWarningsBannerProps {
|
||||
}
|
||||
|
||||
const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _user, onWarningCount }) => {
|
||||
const [record, setRecord] = useState<AtemschutzUebersicht | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [fetchError, setFetchError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
|
||||
const fetchStatus = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setFetchError(null);
|
||||
const data = await atemschutzApi.getMyStatus();
|
||||
if (mounted) setRecord(data);
|
||||
} catch {
|
||||
if (mounted) setFetchError('Persönlicher Atemschutz-Status konnte nicht geladen werden.');
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchStatus();
|
||||
return () => { mounted = false; };
|
||||
}, []);
|
||||
const { data: record, isLoading } = useQuery<AtemschutzUebersicht | null>({
|
||||
queryKey: ['atemschutz-my-status'],
|
||||
queryFn: () => atemschutzApi.getMyStatus(),
|
||||
});
|
||||
|
||||
// ── Loading state ──────────────────────────────────────────────────────────
|
||||
|
||||
if (loading) {
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 1 }}>
|
||||
<CircularProgress size={16} />
|
||||
@@ -111,13 +93,6 @@ const PersonalWarningsBanner: React.FC<PersonalWarningsBannerProps> = ({ user: _
|
||||
);
|
||||
}
|
||||
|
||||
// ── Fetch error ────────────────────────────────────────────────────────────
|
||||
|
||||
if (fetchError) {
|
||||
// Non-critical — silently swallow so the dashboard still loads cleanly.
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── No atemschutz record for this user ─────────────────────────────────────
|
||||
|
||||
if (!record) return null;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React from 'react';
|
||||
import {
|
||||
Box,
|
||||
Card,
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from '@mui/material';
|
||||
import { CalendarMonth as CalendarMonthIcon } from '@mui/icons-material';
|
||||
import { Link as RouterLink } from 'react-router-dom';
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { trainingApi } from '../../services/training';
|
||||
import { eventsApi } from '../../services/events';
|
||||
import type { UebungListItem, UebungTyp } from '../../types/training.types';
|
||||
@@ -106,52 +107,34 @@ const FETCH_LIMIT = 20; // fetch more than 5 so filtering from today leaves enou
|
||||
const DISPLAY_LIMIT = 5;
|
||||
|
||||
const UpcomingEventsWidget: React.FC = () => {
|
||||
const [entries, setEntries] = useState<CalendarEntry[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const { data: trainingItems = [], isLoading: trainingLoading, isError: trainingError } = useQuery<UebungListItem[]>({
|
||||
queryKey: ['upcoming-trainings'],
|
||||
queryFn: () => trainingApi.getUpcoming(FETCH_LIMIT),
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let mounted = true;
|
||||
const { data: eventItems = [], isLoading: eventsLoading, isError: eventsError } = useQuery<VeranstaltungListItem[]>({
|
||||
queryKey: ['upcoming-events'],
|
||||
queryFn: () => eventsApi.getUpcoming(FETCH_LIMIT),
|
||||
});
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
const loading = trainingLoading || eventsLoading;
|
||||
const error = trainingError || eventsError;
|
||||
|
||||
const [trainingItems, eventItems] = await Promise.all([
|
||||
trainingApi.getUpcoming(FETCH_LIMIT),
|
||||
eventsApi.getUpcoming(FETCH_LIMIT),
|
||||
]);
|
||||
|
||||
if (!mounted) return;
|
||||
|
||||
const today = startOfToday();
|
||||
|
||||
const combined: CalendarEntry[] = [
|
||||
...trainingItems
|
||||
.filter((t) => !t.abgesagt)
|
||||
.map(mapTraining),
|
||||
...eventItems
|
||||
.filter((e) => !e.abgesagt)
|
||||
.map(mapVeranstaltung),
|
||||
]
|
||||
.filter((e) => e.date >= today)
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
.slice(0, DISPLAY_LIMIT);
|
||||
|
||||
setEntries(combined);
|
||||
} catch {
|
||||
if (mounted) setError('Termine konnten nicht geladen werden.');
|
||||
} finally {
|
||||
if (mounted) setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
return () => {
|
||||
mounted = false;
|
||||
};
|
||||
}, []);
|
||||
const entries = React.useMemo(() => {
|
||||
if (loading) return [];
|
||||
const today = startOfToday();
|
||||
return [
|
||||
...trainingItems
|
||||
.filter((t) => !t.abgesagt)
|
||||
.map(mapTraining),
|
||||
...eventItems
|
||||
.filter((e) => !e.abgesagt)
|
||||
.map(mapVeranstaltung),
|
||||
]
|
||||
.filter((e) => e.date >= today)
|
||||
.sort((a, b) => a.date.getTime() - b.date.getTime())
|
||||
.slice(0, DISPLAY_LIMIT);
|
||||
}, [trainingItems, eventItems, loading]);
|
||||
|
||||
// ── Loading state ─────────────────────────────────────────────────────────
|
||||
if (loading) {
|
||||
@@ -177,7 +160,7 @@ const UpcomingEventsWidget: React.FC = () => {
|
||||
<Typography variant="h6">Nächste Termine</Typography>
|
||||
</Box>
|
||||
<Typography variant="body2" color="error">
|
||||
{error}
|
||||
Termine konnten nicht geladen werden.
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user