mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 11:31:13 -05:00
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:
170
src/components/admin/CriticalAlertsPanel.tsx
Normal file
170
src/components/admin/CriticalAlertsPanel.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
83
src/components/admin/MonitoringNavCards.tsx
Normal file
83
src/components/admin/MonitoringNavCards.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
116
src/components/admin/MonitoringQuickStats.tsx
Normal file
116
src/components/admin/MonitoringQuickStats.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
138
src/components/admin/RecentActivityTimeline.tsx
Normal file
138
src/components/admin/RecentActivityTimeline.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
141
src/components/admin/SystemHealthStatus.tsx
Normal file
141
src/components/admin/SystemHealthStatus.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user