diff --git a/apps/web/src/components/ui/ErrorState.tsx b/apps/web/src/components/ui/ErrorState.tsx
new file mode 100644
index 0000000..eae8ec1
--- /dev/null
+++ b/apps/web/src/components/ui/ErrorState.tsx
@@ -0,0 +1,24 @@
+import { Button } from './Button';
+
+/**
+ * Einheitlicher Fehlerzustand für fehlgeschlagene Queries — ersetzt stilles
+ * `return null` (weiße Leerseite) durch einen Hinweis + optionalen Retry.
+ */
+export function ErrorState({
+ message = 'Konnte nicht geladen werden.',
+ onRetry,
+}: {
+ message?: string;
+ onRetry?: () => void;
+}) {
+ return (
+
+
{message}
+ {onRetry && (
+
+ )}
+
+ );
+}
diff --git a/apps/web/src/routes/dashboard.tsx b/apps/web/src/routes/dashboard.tsx
index 4bfb8c2..13a2807 100644
--- a/apps/web/src/routes/dashboard.tsx
+++ b/apps/web/src/routes/dashboard.tsx
@@ -4,6 +4,7 @@ import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
+import { ErrorState } from '../components/ui/ErrorState';
import { TeamTimeSection } from '../components/time/TeamTimeSection';
import { MODUS_META, type ProjectModus } from '../lib/project-meta';
import { AlertTriangle } from 'lucide-react';
@@ -23,7 +24,13 @@ export function DashboardRoute() {
);
}
- if (!q.data) return null;
+ if (q.isError || !q.data)
+ return (
+ void q.refetch()}
+ />
+ );
const isCheckedIn =
today.data?.entry?.status !== undefined && today.data.entry.status !== 'absent';
diff --git a/apps/web/src/routes/historie.tsx b/apps/web/src/routes/historie.tsx
index 2429fbe..b8a021a 100644
--- a/apps/web/src/routes/historie.tsx
+++ b/apps/web/src/routes/historie.tsx
@@ -6,6 +6,7 @@ import type { AppRouter } from '@cockpit/api/router';
import { trpc } from '../lib/trpc';
import { Card } from '../components/ui/Card';
import { SkeletonCard } from '../components/ui/SkeletonCard';
+import { ErrorState } from '../components/ui/ErrorState';
type HistoryPerson = inferRouterOutputs['dayEntry']['historyForDate']['entries'][number];
@@ -37,7 +38,13 @@ export function HistorieRoute() {
const q = trpc.dayEntry.historyForDate.useQuery({ date });
if (q.isPending) return ;
- if (!q.data) return null;
+ if (q.isError || !q.data)
+ return (
+ void q.refetch()}
+ />
+ );
const { date: current, prevDate, nextDate, entries, ohneEintrag } = q.data;
return (
diff --git a/apps/web/src/routes/lead.tsx b/apps/web/src/routes/lead.tsx
index 9af5c19..5884075 100644
--- a/apps/web/src/routes/lead.tsx
+++ b/apps/web/src/routes/lead.tsx
@@ -10,6 +10,7 @@ import { voice } from '../lib/voice';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
+import { ErrorState } from '../components/ui/ErrorState';
import { useConfirm } from '../components/ui/ConfirmProvider';
import {
LEAD_STATUS_META,
@@ -72,7 +73,13 @@ export function LeadRoute() {
const q = trpc.lead.byId.useQuery({ id });
if (q.isPending) return ;
- if (!q.data) return null;
+ if (q.isError || !q.data)
+ return (
+ void q.refetch()}
+ />
+ );
const { lead, contacts, touchpoints } = q.data;
function goBack() {
diff --git a/apps/web/src/routes/projekt.tsx b/apps/web/src/routes/projekt.tsx
index 55bf670..e537bbd 100644
--- a/apps/web/src/routes/projekt.tsx
+++ b/apps/web/src/routes/projekt.tsx
@@ -7,6 +7,7 @@ import { trpc } from '../lib/trpc';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
+import { ErrorState } from '../components/ui/ErrorState';
import { useConfirm } from '../components/ui/ConfirmProvider';
import { useCurrentUser } from '../hooks/useCurrentUser';
import { voice } from '../lib/voice';
@@ -71,7 +72,13 @@ export function ProjektRoute() {
const q = trpc.project.byId.useQuery({ id });
const [abschlussOpen, setAbschlussOpen] = useState(false);
if (q.isPending) return ;
- if (!q.data) return null;
+ if (q.isError || !q.data)
+ return (
+ void q.refetch()}
+ />
+ );
const { project: p, area: a, milestones } = q.data;
const isP1 = me?.id === p.verantwortlicheUserId;
const pflichtKeys: Array<{ key: string; label: string }> = [
diff --git a/apps/web/src/routes/team-view.tsx b/apps/web/src/routes/team-view.tsx
index 0f01292..12b967f 100644
--- a/apps/web/src/routes/team-view.tsx
+++ b/apps/web/src/routes/team-view.tsx
@@ -5,6 +5,7 @@ import { trpc } from '../lib/trpc';
import { Card } from '../components/ui/Card';
import { Button } from '../components/ui/Button';
import { SkeletonCard } from '../components/ui/SkeletonCard';
+import { ErrorState } from '../components/ui/ErrorState';
import { formatDurationCompact } from '../lib/format-duration';
import { TIME_CATEGORIES } from '../lib/time-categories';
@@ -124,7 +125,13 @@ export function TeamViewRoute() {
}, [isToday]);
if (q.isPending) return ;
- if (!q.data) return null;
+ if (q.isError || !q.data)
+ return (
+ void q.refetch()}
+ />
+ );
const lanes = q.data.lanes;
const nowMs = Date.now();