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:
Matthias Hochmeister
2026-03-03 12:55:16 +01:00
parent d91f757f34
commit 02cf5138cf
10 changed files with 79 additions and 174 deletions

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import {
Alert,
AlertTitle,
@@ -10,6 +10,7 @@ import {
Typography,
} from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { useQuery } from '@tanstack/react-query';
import { atemschutzApi } from '../../services/atemschutz';
import type { AtemschutzStats } from '../../types/atemschutz.types';
@@ -20,31 +21,12 @@ interface AtemschutzDashboardCardProps {
const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
hideWhenEmpty = false,
}) => {
const [stats, setStats] = useState<AtemschutzStats | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const { data: stats, isLoading, isError } = useQuery<AtemschutzStats>({
queryKey: ['atemschutz-stats'],
queryFn: () => atemschutzApi.getStats(),
});
useEffect(() => {
let mounted = true;
const fetchStats = async () => {
try {
setLoading(true);
setError(null);
const data = await atemschutzApi.getStats();
if (mounted) setStats(data);
} catch {
if (mounted) setError('Atemschutzstatus konnte nicht geladen werden.');
} finally {
if (mounted) setLoading(false);
}
};
fetchStats();
return () => {
mounted = false;
};
}, []);
if (loading) {
if (isLoading) {
return (
<Card>
<CardContent sx={{ display: 'flex', alignItems: 'center', gap: 1, py: 2 }}>
@@ -57,12 +39,12 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
);
}
if (error) {
if (isError) {
return (
<Card>
<CardContent>
<Typography variant="body2" color="error">
{error}
Atemschutzstatus konnte nicht geladen werden.
</Typography>
</CardContent>
</Card>
@@ -71,7 +53,6 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
if (!stats) return null;
// Determine if there are any concerns
const hasConcerns =
stats.untersuchungAbgelaufen > 0 ||
stats.leistungstestAbgelaufen > 0 ||
@@ -80,7 +61,6 @@ const AtemschutzDashboardCard: React.FC<AtemschutzDashboardCardProps> = ({
const allGood = stats.einsatzbereit === stats.total && !hasConcerns;
// If hideWhenEmpty and everything is fine, render nothing
if (hideWhenEmpty && allGood) return null;
return (