Files
pn-new-crm/src/components/admin/system-monitoring-dashboard.tsx
Matt Ciaccio 71da6e8fdc feat(mobile): swap admin page headers to PageHeader
Mechanical sweep replacing the plain h1+p header markup with the
mobile-aware PageHeader primitive across 12 admin pages: index,
backup, branding, documenso, email, import, invitations, monitoring,
onboarding, reminders, reports, webhooks. Webhooks "Add Webhook"
button preserved via the actions slot.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-01 12:57:52 +02:00

187 lines
6.2 KiB
TypeScript

'use client';
import { useQuery } from '@tanstack/react-query';
import { Activity, Wifi, AlertTriangle, CheckCircle2 } from 'lucide-react';
import { apiFetch } from '@/lib/api/client';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { ServiceHealthCard } from './service-health-card';
import { QueueOverview } from './queue-overview';
import { PageHeader } from '@/components/shared/page-header';
import type {
HealthStatus,
QueueStatus,
ConnectionStatus,
RecentError,
} from '@/lib/services/system-monitoring.service';
export function SystemMonitoringDashboard() {
const { data: healthData } = useQuery({
queryKey: ['system', 'health'],
queryFn: () => apiFetch<{ data: HealthStatus }>('/api/v1/admin/health').then((r) => r.data),
staleTime: 30_000,
refetchInterval: 30_000,
});
const { data: queuesData } = useQuery({
queryKey: ['system', 'queues'],
queryFn: () => apiFetch<{ data: QueueStatus[] }>('/api/v1/admin/queues').then((r) => r.data),
staleTime: 10_000,
refetchInterval: 10_000,
});
const { data: connectionsData } = useQuery({
queryKey: ['system', 'connections'],
queryFn: () =>
apiFetch<{ data: ConnectionStatus }>('/api/v1/admin/connections').then((r) => r.data),
staleTime: 30_000,
refetchInterval: 30_000,
});
const queues: QueueStatus[] = queuesData ?? [];
const health: HealthStatus | undefined = healthData;
const connections = connectionsData?.totalConnections ?? 0;
const totalFailed = queues.reduce((sum, q) => sum + q.failed, 0);
return (
<div className="space-y-8">
<PageHeader
title="System Monitoring"
description="Real-time health, queue status and connection tracking"
/>
{/* Service health */}
<section className="space-y-3">
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
Service Health
</h2>
{health ? (
<>
<div className="flex items-center gap-2 mb-3">
{health.overall === 'healthy' ? (
<CheckCircle2 className="h-4 w-4 text-green-500" />
) : (
<AlertTriangle className="h-4 w-4 text-yellow-500" />
)}
<span className="text-sm text-muted-foreground">
All services checked at {new Date(health.checkedAt).toLocaleTimeString()}
</span>
</div>
<div className="flex flex-wrap gap-3">
{health.services.map((service) => (
<ServiceHealthCard key={service.name} service={service} />
))}
</div>
</>
) : (
<div className="flex gap-3">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="h-[88px] w-[160px] rounded-xl border bg-card animate-pulse" />
))}
</div>
)}
</section>
{/* Stats row */}
<section className="grid grid-cols-2 sm:grid-cols-3 gap-4">
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
<Wifi className="h-3.5 w-3.5" />
Active Connections
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{connections}</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
<AlertTriangle className="h-3.5 w-3.5" />
Total Failed Jobs
</CardTitle>
</CardHeader>
<CardContent>
<p className={`text-3xl font-bold ${totalFailed > 0 ? 'text-destructive' : ''}`}>
{totalFailed}
</p>
</CardContent>
</Card>
<Card>
<CardHeader className="pb-2">
<CardTitle className="text-xs text-muted-foreground font-medium uppercase tracking-wide flex items-center gap-1.5">
<Activity className="h-3.5 w-3.5" />
Active Jobs
</CardTitle>
</CardHeader>
<CardContent>
<p className="text-3xl font-bold">{queues.reduce((sum, q) => sum + q.active, 0)}</p>
</CardContent>
</Card>
</section>
{/* Queue overview */}
<section className="space-y-3">
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
Queue Overview
</h2>
{queues.length > 0 ? (
<QueueOverview queues={queues} />
) : (
<div className="grid grid-cols-5 gap-3">
{[1, 2, 3, 4, 5].map((i) => (
<div key={i} className="h-[110px] rounded-xl border bg-card animate-pulse" />
))}
</div>
)}
</section>
{/* Recent errors */}
<RecentErrorsPanel />
</div>
);
}
function RecentErrorsPanel() {
const { data: errorsData } = useQuery({
queryKey: ['system', 'errors'],
queryFn: () => apiFetch<{ data: RecentError[] }>('/api/v1/admin/errors').then((r) => r.data),
staleTime: 30_000,
refetchInterval: 30_000,
});
const errors: RecentError[] = errorsData ?? [];
return (
<section className="space-y-3">
<h2 className="text-sm font-semibold text-muted-foreground uppercase tracking-wide">
Recent Errors
</h2>
{errors.length === 0 ? (
<p className="text-sm text-muted-foreground">No recent errors.</p>
) : (
<div className="space-y-2">
{errors.map((error) => (
<div
key={error.id}
className="flex items-start justify-between gap-4 rounded-lg border px-4 py-3 text-sm"
>
<div className="space-y-0.5 min-w-0">
<p className="font-medium truncate">{error.message}</p>
<p className="text-xs text-muted-foreground">
{error.source === 'queue' ? 'Queue' : 'Audit'} &mdash;{' '}
{new Date(error.timestamp).toLocaleString()}
</p>
</div>
</div>
))}
</div>
)}
</section>
);
}