Implement monitoring overview features

Add comprehensive monitoring dashboard scaffolding:
- Extend queryKeys with monitoring keys
- Create hooks: useCombinedAlerts, useRecentActivity, useDatabaseHealth, useModerationHealth
- Build UI components: SystemHealthStatus, CriticalAlertsPanel, MonitoringQuickStats, RecentActivityTimeline, MonitoringNavCards
- Create MonitoringOverview page and integrate routing (MonitoringOverview route)
- Wire AdminSidebar to include Monitoring navigation
- Introduce supporting routing and admin layout hooks/utilities
- Align with TanStack Query patterns and plan for auto-refresh and optimistic updates
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 01:33:53 +00:00
parent d94062a937
commit 99c917deaf
14 changed files with 1045 additions and 11 deletions

View File

@@ -0,0 +1,170 @@
import { AlertTriangle, CheckCircle2, Clock, ShieldAlert, XCircle } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { formatDistanceToNow } from 'date-fns';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { supabase } from '@/integrations/supabase/client';
import { toast } from 'sonner';
import { Link } from 'react-router-dom';
import type { CombinedAlert } from '@/hooks/admin/useCombinedAlerts';
interface CriticalAlertsPanelProps {
alerts?: CombinedAlert[];
isLoading: boolean;
}
const SEVERITY_CONFIG = {
critical: { color: 'destructive' as const, icon: XCircle, label: 'Critical' },
high: { color: 'destructive' as const, icon: AlertTriangle, label: 'High' },
medium: { color: 'secondary' as const, icon: Clock, label: 'Medium' },
low: { color: 'secondary' as const, icon: Clock, label: 'Low' },
};
export function CriticalAlertsPanel({ alerts, isLoading }: CriticalAlertsPanelProps) {
const queryClient = useQueryClient();
const resolveSystemAlert = useMutation({
mutationFn: async (alertId: string) => {
const { error } = await supabase
.from('system_alerts')
.update({ resolved_at: new Date().toISOString() })
.eq('id', alertId);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['system-alerts'] });
queryClient.invalidateQueries({ queryKey: ['monitoring'] });
toast.success('Alert resolved');
},
onError: () => {
toast.error('Failed to resolve alert');
},
});
const resolveRateLimitAlert = useMutation({
mutationFn: async (alertId: string) => {
const { error } = await supabase
.from('rate_limit_alerts')
.update({ resolved_at: new Date().toISOString() })
.eq('id', alertId);
if (error) throw error;
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['rate-limit-alerts'] });
queryClient.invalidateQueries({ queryKey: ['monitoring'] });
toast.success('Alert resolved');
},
onError: () => {
toast.error('Failed to resolve alert');
},
});
const handleResolve = (alert: CombinedAlert) => {
if (alert.source === 'system') {
resolveSystemAlert.mutate(alert.id);
} else {
resolveRateLimitAlert.mutate(alert.id);
}
};
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ShieldAlert className="w-5 h-5" />
Critical Alerts
</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center text-muted-foreground py-8">Loading alerts...</div>
</CardContent>
</Card>
);
}
if (!alerts || alerts.length === 0) {
return (
<Card className="border-green-500/20">
<CardHeader>
<CardTitle className="flex items-center gap-2">
<ShieldAlert className="w-5 h-5" />
Critical Alerts
</CardTitle>
</CardHeader>
<CardContent>
<div className="flex items-center gap-3 p-4 rounded-lg bg-green-500/10">
<CheckCircle2 className="w-8 h-8 text-green-500" />
<div>
<div className="font-semibold">All Systems Operational</div>
<div className="text-sm text-muted-foreground">No active alerts detected</div>
</div>
</div>
</CardContent>
</Card>
);
}
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<ShieldAlert className="w-5 h-5" />
Critical Alerts
<Badge variant="destructive">{alerts.length}</Badge>
</CardTitle>
<div className="flex gap-2">
<Button asChild size="sm" variant="ghost">
<Link to="/admin/error-monitoring">View All</Link>
</Button>
</div>
</div>
</CardHeader>
<CardContent className="space-y-2">
{alerts.map((alert) => {
const config = SEVERITY_CONFIG[alert.severity];
const SeverityIcon = config.icon;
return (
<div
key={alert.id}
className="flex items-start gap-3 p-3 rounded-lg border border-border hover:bg-accent/50 transition-colors"
>
<SeverityIcon className={`w-5 h-5 mt-0.5 flex-shrink-0 ${alert.severity === 'critical' || alert.severity === 'high' ? 'text-destructive' : 'text-muted-foreground'}`} />
<div className="flex-1 min-w-0">
<div className="flex items-start gap-2 flex-wrap">
<Badge variant={config.color} className="flex-shrink-0">
{config.label}
</Badge>
<Badge variant="outline" className="flex-shrink-0">
{alert.source === 'system' ? 'System' : 'Rate Limit'}
</Badge>
{alert.alert_type && (
<span className="text-xs text-muted-foreground">
{alert.alert_type.replace(/_/g, ' ')}
</span>
)}
</div>
<p className="text-sm mt-1 break-words">{alert.message}</p>
<p className="text-xs text-muted-foreground mt-1">
{formatDistanceToNow(new Date(alert.created_at), { addSuffix: true })}
</p>
</div>
<Button
size="sm"
variant="outline"
onClick={() => handleResolve(alert)}
loading={resolveSystemAlert.isPending || resolveRateLimitAlert.isPending}
className="flex-shrink-0"
>
Resolve
</Button>
</div>
);
})}
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,83 @@
import { AlertTriangle, ArrowRight, ScrollText, Shield } from 'lucide-react';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Link } from 'react-router-dom';
interface NavCardProps {
title: string;
description: string;
to: string;
icon: React.ComponentType<{ className?: string }>;
stat?: string;
badge?: number;
}
function NavCard({ title, description, to, icon: Icon, stat, badge }: NavCardProps) {
return (
<Link to={to}>
<Card className="hover:bg-accent/50 transition-colors cursor-pointer h-full">
<CardHeader>
<div className="flex items-start justify-between">
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-primary/10">
<Icon className="w-5 h-5 text-primary" />
</div>
<div>
<CardTitle className="text-base flex items-center gap-2">
{title}
{badge !== undefined && badge > 0 && (
<Badge variant="destructive" className="text-xs">
{badge}
</Badge>
)}
</CardTitle>
</div>
</div>
<ArrowRight className="w-5 h-5 text-muted-foreground" />
</div>
<CardDescription>{description}</CardDescription>
</CardHeader>
{stat && (
<CardContent>
<p className="text-sm text-muted-foreground">{stat}</p>
</CardContent>
)}
</Card>
</Link>
);
}
interface MonitoringNavCardsProps {
errorCount?: number;
rateLimitCount?: number;
}
export function MonitoringNavCards({ errorCount, rateLimitCount }: MonitoringNavCardsProps) {
return (
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<NavCard
title="Error Monitoring"
description="View detailed error logs, analytics, and traces"
to="/admin/error-monitoring"
icon={AlertTriangle}
stat={errorCount !== undefined ? `${errorCount} errors in last 24h` : undefined}
badge={errorCount}
/>
<NavCard
title="Rate Limit Metrics"
description="Monitor rate limiting, alerts, and configurations"
to="/admin/rate-limit-metrics"
icon={Shield}
stat={rateLimitCount !== undefined ? `${rateLimitCount} blocks today` : undefined}
/>
<NavCard
title="System Log"
description="View system events, audit trails, and history"
to="/admin/system-log"
icon={ScrollText}
/>
</div>
);
}

View File

@@ -0,0 +1,116 @@
import { Activity, AlertTriangle, Clock, Database, FileText, Shield, TrendingUp, Users } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import type { SystemHealthData } from '@/hooks/useSystemHealth';
import type { ModerationHealth } from '@/hooks/admin/useModerationHealth';
interface MonitoringQuickStatsProps {
systemHealth?: SystemHealthData;
rateLimitStats?: { total_requests: number; blocked_requests: number; unique_ips: number };
moderationHealth?: ModerationHealth;
}
interface StatCardProps {
icon: React.ComponentType<{ className?: string }>;
label: string;
value: string | number;
trend?: 'up' | 'down' | 'neutral';
status?: 'healthy' | 'warning' | 'critical';
}
function StatCard({ icon: Icon, label, value, status = 'healthy' }: StatCardProps) {
const statusColors = {
healthy: 'text-green-500',
warning: 'text-yellow-500',
critical: 'text-red-500',
};
return (
<Card>
<CardContent className="p-4">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg bg-muted ${statusColors[status]}`}>
<Icon className="w-5 h-5" />
</div>
<div className="flex-1 min-w-0">
<p className="text-xs text-muted-foreground truncate">{label}</p>
<p className="text-2xl font-bold">{value}</p>
</div>
</div>
</CardContent>
</Card>
);
}
export function MonitoringQuickStats({ systemHealth, rateLimitStats, moderationHealth }: MonitoringQuickStatsProps) {
const criticalAlerts = systemHealth?.critical_alerts_count || 0;
const highAlerts = systemHealth?.high_alerts_count || 0;
const totalAlerts = criticalAlerts + highAlerts;
const blockRate = rateLimitStats?.total_requests
? ((rateLimitStats.blocked_requests / rateLimitStats.total_requests) * 100).toFixed(1)
: '0.0';
const queueStatus =
(moderationHealth?.queueLength || 0) > 50 ? 'critical' :
(moderationHealth?.queueLength || 0) > 20 ? 'warning' : 'healthy';
return (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<StatCard
icon={AlertTriangle}
label="Active Alerts"
value={totalAlerts}
status={criticalAlerts > 0 ? 'critical' : highAlerts > 0 ? 'warning' : 'healthy'}
/>
<StatCard
icon={Shield}
label="Rate Limit Block Rate"
value={`${blockRate}%`}
status={parseFloat(blockRate) > 5 ? 'warning' : 'healthy'}
/>
<StatCard
icon={FileText}
label="Moderation Queue"
value={moderationHealth?.queueLength || 0}
status={queueStatus}
/>
<StatCard
icon={Clock}
label="Active Locks"
value={moderationHealth?.activeLocks || 0}
status={(moderationHealth?.activeLocks || 0) > 5 ? 'warning' : 'healthy'}
/>
<StatCard
icon={Database}
label="Orphaned Images"
value={systemHealth?.orphaned_images_count || 0}
status={(systemHealth?.orphaned_images_count || 0) > 0 ? 'warning' : 'healthy'}
/>
<StatCard
icon={Activity}
label="Failed Webhooks"
value={systemHealth?.failed_webhook_count || 0}
status={(systemHealth?.failed_webhook_count || 0) > 0 ? 'warning' : 'healthy'}
/>
<StatCard
icon={Users}
label="Unique IPs"
value={rateLimitStats?.unique_ips || 0}
status="healthy"
/>
<StatCard
icon={TrendingUp}
label="Total Requests"
value={rateLimitStats?.total_requests || 0}
status="healthy"
/>
</div>
);
}

View File

@@ -0,0 +1,138 @@
import { AlertTriangle, Database, ShieldAlert, XCircle } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { ScrollArea } from '@/components/ui/scroll-area';
import { formatDistanceToNow } from 'date-fns';
import { Link } from 'react-router-dom';
import type { ActivityEvent } from '@/hooks/admin/useRecentActivity';
interface RecentActivityTimelineProps {
activity?: ActivityEvent[];
isLoading: boolean;
}
export function RecentActivityTimeline({ activity, isLoading }: RecentActivityTimelineProps) {
if (isLoading) {
return (
<Card>
<CardHeader>
<CardTitle>Recent Activity</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center text-muted-foreground py-8">Loading activity...</div>
</CardContent>
</Card>
);
}
if (!activity || activity.length === 0) {
return (
<Card>
<CardHeader>
<CardTitle>Recent Activity (Last Hour)</CardTitle>
</CardHeader>
<CardContent>
<div className="text-center text-muted-foreground py-8">No recent activity</div>
</CardContent>
</Card>
);
}
const getEventIcon = (event: ActivityEvent) => {
switch (event.type) {
case 'error':
return XCircle;
case 'approval':
return Database;
case 'alert':
return AlertTriangle;
}
};
const getEventColor = (event: ActivityEvent) => {
switch (event.type) {
case 'error':
return 'text-red-500';
case 'approval':
return 'text-orange-500';
case 'alert':
return 'text-yellow-500';
}
};
const getEventDescription = (event: ActivityEvent) => {
switch (event.type) {
case 'error':
return `${event.error_type} in ${event.endpoint}`;
case 'approval':
return `Approval failed: ${event.error_message}`;
case 'alert':
return event.message;
}
};
const getEventLink = (event: ActivityEvent) => {
switch (event.type) {
case 'error':
return `/admin/error-monitoring`;
case 'approval':
return `/admin/error-monitoring?tab=approvals`;
case 'alert':
return `/admin/error-monitoring`;
default:
return undefined;
}
};
return (
<Card>
<CardHeader>
<div className="flex items-center justify-between">
<CardTitle>Recent Activity (Last Hour)</CardTitle>
<Badge variant="outline">{activity.length} events</Badge>
</div>
</CardHeader>
<CardContent>
<ScrollArea className="h-[400px] pr-4">
<div className="space-y-3">
{activity.map((event) => {
const Icon = getEventIcon(event);
const color = getEventColor(event);
const description = getEventDescription(event);
const link = getEventLink(event);
const content = (
<div
className={`flex items-start gap-3 p-3 rounded-lg border border-border transition-colors ${
link ? 'hover:bg-accent/50 cursor-pointer' : ''
}`}
>
<Icon className={`w-5 h-5 mt-0.5 flex-shrink-0 ${color}`} />
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<Badge variant="outline" className="text-xs capitalize">
{event.type}
</Badge>
<span className="text-xs text-muted-foreground">
{formatDistanceToNow(new Date(event.created_at), { addSuffix: true })}
</span>
</div>
<p className="text-sm mt-1 break-words">{description}</p>
</div>
</div>
);
return link ? (
<Link key={event.id} to={link}>
{content}
</Link>
) : (
<div key={event.id}>{content}</div>
);
})}
</div>
</ScrollArea>
</CardContent>
</Card>
);
}

View File

@@ -0,0 +1,141 @@
import { Activity, AlertTriangle, CheckCircle2, XCircle } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { useRunSystemMaintenance, type SystemHealthData } from '@/hooks/useSystemHealth';
import type { DatabaseHealth } from '@/hooks/admin/useDatabaseHealth';
interface SystemHealthStatusProps {
systemHealth?: SystemHealthData;
dbHealth?: DatabaseHealth;
isLoading: boolean;
}
export function SystemHealthStatus({ systemHealth, dbHealth, isLoading }: SystemHealthStatusProps) {
const runMaintenance = useRunSystemMaintenance();
const getOverallStatus = () => {
if (isLoading) return 'checking';
if (!systemHealth) return 'unknown';
const hasCriticalIssues =
(systemHealth.orphaned_images_count || 0) > 0 ||
(systemHealth.failed_webhook_count || 0) > 0 ||
(systemHealth.critical_alerts_count || 0) > 0 ||
dbHealth?.status === 'unhealthy';
if (hasCriticalIssues) return 'unhealthy';
const hasWarnings =
dbHealth?.status === 'warning' ||
(systemHealth.high_alerts_count || 0) > 0;
if (hasWarnings) return 'warning';
return 'healthy';
};
const status = getOverallStatus();
const statusConfig = {
healthy: {
icon: CheckCircle2,
label: 'All Systems Operational',
color: 'text-green-500',
bgColor: 'bg-green-500/10',
borderColor: 'border-green-500/20',
},
warning: {
icon: AlertTriangle,
label: 'System Warning',
color: 'text-yellow-500',
bgColor: 'bg-yellow-500/10',
borderColor: 'border-yellow-500/20',
},
unhealthy: {
icon: XCircle,
label: 'Critical Issues Detected',
color: 'text-red-500',
bgColor: 'bg-red-500/10',
borderColor: 'border-red-500/20',
},
checking: {
icon: Activity,
label: 'Checking System Health...',
color: 'text-muted-foreground',
bgColor: 'bg-muted',
borderColor: 'border-border',
},
unknown: {
icon: AlertTriangle,
label: 'Unable to Determine Status',
color: 'text-muted-foreground',
bgColor: 'bg-muted',
borderColor: 'border-border',
},
};
const config = statusConfig[status];
const StatusIcon = config.icon;
const handleRunMaintenance = () => {
runMaintenance.mutate();
};
return (
<Card className={`${config.borderColor} border-2`}>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="flex items-center gap-2">
<Activity className="w-5 h-5" />
System Health
</CardTitle>
{(status === 'unhealthy' || status === 'warning') && (
<Button
size="sm"
variant="outline"
onClick={handleRunMaintenance}
loading={runMaintenance.isPending}
loadingText="Running..."
>
Run Maintenance
</Button>
)}
</div>
</CardHeader>
<CardContent>
<div className={`flex items-center gap-3 p-4 rounded-lg ${config.bgColor}`}>
<StatusIcon className={`w-8 h-8 ${config.color}`} />
<div className="flex-1">
<div className="flex items-center gap-2">
<span className="font-semibold">{config.label}</span>
<Badge variant={status === 'healthy' ? 'default' : status === 'warning' ? 'secondary' : 'destructive'}>
{status.toUpperCase()}
</Badge>
</div>
{systemHealth && (
<div className="mt-2 grid grid-cols-2 sm:grid-cols-4 gap-2 text-sm">
<div>
<span className="text-muted-foreground">Orphaned Images:</span>
<span className="ml-1 font-medium">{systemHealth.orphaned_images_count || 0}</span>
</div>
<div>
<span className="text-muted-foreground">Failed Webhooks:</span>
<span className="ml-1 font-medium">{systemHealth.failed_webhook_count || 0}</span>
</div>
<div>
<span className="text-muted-foreground">Critical Alerts:</span>
<span className="ml-1 font-medium">{systemHealth.critical_alerts_count || 0}</span>
</div>
<div>
<span className="text-muted-foreground">DB Errors (1h):</span>
<span className="ml-1 font-medium">{dbHealth?.recentErrors || 0}</span>
</div>
</div>
)}
</div>
</div>
</CardContent>
</Card>
);
}