diff --git a/src/App.tsx b/src/App.tsx index 3847e6fc..d3931acd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -75,6 +75,7 @@ const ErrorMonitoring = lazy(() => import("./pages/admin/ErrorMonitoring")); const ErrorLookup = lazy(() => import("./pages/admin/ErrorLookup")); const TraceViewer = lazy(() => import("./pages/admin/TraceViewer")); const RateLimitMetrics = lazy(() => import("./pages/admin/RateLimitMetrics")); +const MonitoringOverview = lazy(() => import("./pages/admin/MonitoringOverview")); // User routes (lazy-loaded) const Profile = lazy(() => import("./pages/Profile")); @@ -405,6 +406,14 @@ function AppContent(): React.JSX.Element { } /> + + + + } + /> {/* Utility routes - lazy loaded */} } /> diff --git a/src/components/admin/CriticalAlertsPanel.tsx b/src/components/admin/CriticalAlertsPanel.tsx new file mode 100644 index 00000000..be649ad1 --- /dev/null +++ b/src/components/admin/CriticalAlertsPanel.tsx @@ -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 ( + + + + + Critical Alerts + + + +
Loading alerts...
+
+
+ ); + } + + if (!alerts || alerts.length === 0) { + return ( + + + + + Critical Alerts + + + +
+ +
+
All Systems Operational
+
No active alerts detected
+
+
+
+
+ ); + } + + return ( + + +
+ + + Critical Alerts + {alerts.length} + +
+ +
+
+
+ + {alerts.map((alert) => { + const config = SEVERITY_CONFIG[alert.severity]; + const SeverityIcon = config.icon; + + return ( +
+ +
+
+ + {config.label} + + + {alert.source === 'system' ? 'System' : 'Rate Limit'} + + {alert.alert_type && ( + + {alert.alert_type.replace(/_/g, ' ')} + + )} +
+

{alert.message}

+

+ {formatDistanceToNow(new Date(alert.created_at), { addSuffix: true })} +

+
+ +
+ ); + })} +
+
+ ); +} diff --git a/src/components/admin/MonitoringNavCards.tsx b/src/components/admin/MonitoringNavCards.tsx new file mode 100644 index 00000000..29ca21cd --- /dev/null +++ b/src/components/admin/MonitoringNavCards.tsx @@ -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 ( + + + +
+
+
+ +
+
+ + {title} + {badge !== undefined && badge > 0 && ( + + {badge} + + )} + +
+
+ +
+ {description} +
+ {stat && ( + +

{stat}

+
+ )} +
+ + ); +} + +interface MonitoringNavCardsProps { + errorCount?: number; + rateLimitCount?: number; +} + +export function MonitoringNavCards({ errorCount, rateLimitCount }: MonitoringNavCardsProps) { + return ( +
+ + + + + +
+ ); +} diff --git a/src/components/admin/MonitoringQuickStats.tsx b/src/components/admin/MonitoringQuickStats.tsx new file mode 100644 index 00000000..fa12bd3d --- /dev/null +++ b/src/components/admin/MonitoringQuickStats.tsx @@ -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 ( + + +
+
+ +
+
+

{label}

+

{value}

+
+
+
+
+ ); +} + +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 ( +
+ 0 ? 'critical' : highAlerts > 0 ? 'warning' : 'healthy'} + /> + + 5 ? 'warning' : 'healthy'} + /> + + + + 5 ? 'warning' : 'healthy'} + /> + + 0 ? 'warning' : 'healthy'} + /> + + 0 ? 'warning' : 'healthy'} + /> + + + + +
+ ); +} diff --git a/src/components/admin/RecentActivityTimeline.tsx b/src/components/admin/RecentActivityTimeline.tsx new file mode 100644 index 00000000..8ac07c77 --- /dev/null +++ b/src/components/admin/RecentActivityTimeline.tsx @@ -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 ( + + + Recent Activity + + +
Loading activity...
+
+
+ ); + } + + if (!activity || activity.length === 0) { + return ( + + + Recent Activity (Last Hour) + + +
No recent activity
+
+
+ ); + } + + 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 ( + + +
+ Recent Activity (Last Hour) + {activity.length} events +
+
+ + +
+ {activity.map((event) => { + const Icon = getEventIcon(event); + const color = getEventColor(event); + const description = getEventDescription(event); + const link = getEventLink(event); + + const content = ( +
+ +
+
+ + {event.type} + + + {formatDistanceToNow(new Date(event.created_at), { addSuffix: true })} + +
+

{description}

+
+
+ ); + + return link ? ( + + {content} + + ) : ( +
{content}
+ ); + })} +
+
+
+
+ ); +} diff --git a/src/components/admin/SystemHealthStatus.tsx b/src/components/admin/SystemHealthStatus.tsx new file mode 100644 index 00000000..73fb44cc --- /dev/null +++ b/src/components/admin/SystemHealthStatus.tsx @@ -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 ( + + +
+ + + System Health + + {(status === 'unhealthy' || status === 'warning') && ( + + )} +
+
+ +
+ +
+
+ {config.label} + + {status.toUpperCase()} + +
+ {systemHealth && ( +
+
+ Orphaned Images: + {systemHealth.orphaned_images_count || 0} +
+
+ Failed Webhooks: + {systemHealth.failed_webhook_count || 0} +
+
+ Critical Alerts: + {systemHealth.critical_alerts_count || 0} +
+
+ DB Errors (1h): + {dbHealth?.recentErrors || 0} +
+
+ )} +
+
+
+
+ ); +} diff --git a/src/components/layout/AdminSidebar.tsx b/src/components/layout/AdminSidebar.tsx index 066aa4ec..dd8d544e 100644 --- a/src/components/layout/AdminSidebar.tsx +++ b/src/components/layout/AdminSidebar.tsx @@ -1,7 +1,9 @@ -import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle, Shield } from 'lucide-react'; +import { LayoutDashboard, FileText, Flag, Users, Settings, ArrowLeft, ScrollText, BookOpen, Inbox, Mail, AlertTriangle, Shield, Activity } from 'lucide-react'; import { NavLink } from 'react-router-dom'; import { useUserRole } from '@/hooks/useUserRole'; import { useSidebar } from '@/hooks/useSidebar'; +import { useCombinedAlerts } from '@/hooks/admin/useCombinedAlerts'; +import { Badge } from '@/components/ui/badge'; import { Sidebar, SidebarContent, @@ -21,6 +23,8 @@ export function AdminSidebar() { const isSuperuser = permissions?.role_level === 'superuser'; const isAdmin = permissions?.role_level === 'admin' || isSuperuser; const collapsed = state === 'collapsed'; + const { data: combinedAlerts } = useCombinedAlerts(); + const alertCount = combinedAlerts?.length || 0; const navItems = [ { @@ -28,6 +32,12 @@ export function AdminSidebar() { url: '/admin', icon: LayoutDashboard, }, + { + title: 'Monitoring Overview', + url: '/admin/monitoring-overview', + icon: Activity, + badge: alertCount > 0 ? alertCount : undefined, + }, { title: 'Moderation', url: '/admin/moderation', @@ -132,7 +142,21 @@ export function AdminSidebar() { } > - {!collapsed && {item.title}} + {!collapsed && ( + + {item.title} + {item.badge !== undefined && ( + + {item.badge} + + )} + + )} + {collapsed && item.badge !== undefined && item.badge > 0 && ( + + {item.badge} + + )} diff --git a/src/hooks/admin/useCombinedAlerts.ts b/src/hooks/admin/useCombinedAlerts.ts new file mode 100644 index 00000000..9cde5aaf --- /dev/null +++ b/src/hooks/admin/useCombinedAlerts.ts @@ -0,0 +1,49 @@ +import { useQuery } from '@tanstack/react-query'; +import { useSystemAlerts } from '@/hooks/useSystemHealth'; +import { useUnresolvedAlerts } from '@/hooks/useRateLimitAlerts'; +import { queryKeys } from '@/lib/queryKeys'; + +export interface CombinedAlert { + id: string; + created_at: string; + severity: 'critical' | 'high' | 'medium' | 'low'; + message: string; + alert_type?: string; + source: 'system' | 'rate_limit'; + resolved_at?: string | null; + metric_type?: string; + function_name?: string; +} + +export function useCombinedAlerts() { + const systemCritical = useSystemAlerts('critical'); + const systemHigh = useSystemAlerts('high'); + const rateLimitAlerts = useUnresolvedAlerts(); + + return useQuery({ + queryKey: queryKeys.monitoring.combinedAlerts(), + queryFn: () => { + const combined: CombinedAlert[] = [ + ...(systemCritical.data || []).map(a => ({ ...a, source: 'system' as const })), + ...(systemHigh.data || []).map(a => ({ ...a, source: 'system' as const })), + ...(rateLimitAlerts.data || []).map(a => ({ + id: a.id, + created_at: a.created_at, + severity: 'high' as const, // Rate limit alerts are considered high severity + message: a.alert_message, + alert_type: a.metric_type, + source: 'rate_limit' as const, + resolved_at: a.resolved_at, + metric_type: a.metric_type, + function_name: a.function_name, + })), + ]; + return combined + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .slice(0, 10); + }, + enabled: !systemCritical.isLoading && !systemHigh.isLoading && !rateLimitAlerts.isLoading, + staleTime: 15000, + refetchInterval: 30000, + }); +} diff --git a/src/hooks/admin/useDatabaseHealth.ts b/src/hooks/admin/useDatabaseHealth.ts new file mode 100644 index 00000000..1a7946ef --- /dev/null +++ b/src/hooks/admin/useDatabaseHealth.ts @@ -0,0 +1,43 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; +import { queryKeys } from '@/lib/queryKeys'; + +export interface DatabaseHealth { + status: 'healthy' | 'warning' | 'unhealthy'; + recentErrors: number; + checked_at: string; +} + +export function useDatabaseHealth() { + return useQuery({ + queryKey: queryKeys.monitoring.databaseHealth(), + queryFn: async () => { + const threshold = new Date(Date.now() - 3600000); // 1 hour + + // Check for recent database errors + const { count, error } = await supabase + .from('request_metadata') + .select('*', { count: 'exact', head: true }) + .eq('error_type', 'database_error') + .gte('created_at', threshold.toISOString()); + + if (error) { + return { + status: 'warning' as const, + recentErrors: 0, + checked_at: new Date().toISOString(), + }; + } + + const errorCount = count || 0; + + return { + status: errorCount > 10 ? 'unhealthy' : errorCount > 5 ? 'warning' : 'healthy', + recentErrors: errorCount, + checked_at: new Date().toISOString(), + } as DatabaseHealth; + }, + staleTime: 60000, + refetchInterval: 120000, + }); +} diff --git a/src/hooks/admin/useModerationHealth.ts b/src/hooks/admin/useModerationHealth.ts new file mode 100644 index 00000000..334dc3a8 --- /dev/null +++ b/src/hooks/admin/useModerationHealth.ts @@ -0,0 +1,36 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; +import { queryKeys } from '@/lib/queryKeys'; + +export interface ModerationHealth { + queueLength: number; + activeLocks: number; +} + +export function useModerationHealth() { + return useQuery({ + queryKey: queryKeys.monitoring.moderationHealth(), + queryFn: async () => { + const [queue, oldestSubmission] = await Promise.all([ + supabase + .from('content_submissions') + .select('id', { count: 'exact', head: true }) + .eq('status', 'pending_review'), + supabase + .from('content_submissions') + .select('created_at') + .eq('status', 'pending_review') + .order('created_at', { ascending: true }) + .limit(1) + .single(), + ]); + + return { + queueLength: queue.count || 0, + activeLocks: 0, // Not tracking locks for now + } as ModerationHealth; + }, + staleTime: 30000, + refetchInterval: 60000, + }); +} diff --git a/src/hooks/admin/useRecentActivity.ts b/src/hooks/admin/useRecentActivity.ts new file mode 100644 index 00000000..2c7ef278 --- /dev/null +++ b/src/hooks/admin/useRecentActivity.ts @@ -0,0 +1,77 @@ +import { useQuery } from '@tanstack/react-query'; +import { supabase } from '@/integrations/supabase/client'; +import { queryKeys } from '@/lib/queryKeys'; + +export type ActivityEvent = + | { id: string; created_at: string; type: 'error'; error_type: string | null; error_message: string | null; endpoint: string } + | { id: string; created_at: string; type: 'approval'; success: false; error_message: string | null; moderator_id: string } + | { id: string; created_at: string; type: 'alert'; alert_type: string; severity: string; message: string }; + +export function useRecentActivity(timeWindow = 3600000) { // 1 hour default + return useQuery({ + queryKey: queryKeys.monitoring.recentActivity(timeWindow), + queryFn: async () => { + const threshold = new Date(Date.now() - timeWindow); + + const [errors, approvals, alerts] = await Promise.all([ + supabase + .from('request_metadata') + .select('id, created_at, error_type, error_message, endpoint') + .not('error_type', 'is', null) + .gte('created_at', threshold.toISOString()) + .order('created_at', { ascending: false }) + .limit(10), + supabase + .from('approval_transaction_metrics') + .select('id, created_at, success, error_message, moderator_id') + .eq('success', false) + .gte('created_at', threshold.toISOString()) + .order('created_at', { ascending: false }) + .limit(10), + supabase + .from('system_alerts') + .select('id, created_at, alert_type, severity, message') + .gte('created_at', threshold.toISOString()) + .order('created_at', { ascending: false }) + .limit(10), + ]); + + const combined: ActivityEvent[] = [ + ...(errors.data || []) + .filter(e => e.error_type && e.error_message) + .map(e => ({ + id: e.id, + created_at: e.created_at, + type: 'error' as const, + error_type: e.error_type, + error_message: e.error_message, + endpoint: e.endpoint, + })), + ...(approvals.data || []) + .filter(a => a.created_at && a.error_message) + .map(a => ({ + id: a.id, + created_at: a.created_at || new Date().toISOString(), + type: 'approval' as const, + success: false as const, + error_message: a.error_message, + moderator_id: a.moderator_id, + })), + ...(alerts.data || []).map(a => ({ + id: a.id, + created_at: a.created_at, + type: 'alert' as const, + alert_type: a.alert_type, + severity: a.severity, + message: a.message, + })), + ]; + + return combined + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .slice(0, 30); + }, + staleTime: 30000, + refetchInterval: 60000, + }); +} diff --git a/src/hooks/useSystemHealth.ts b/src/hooks/useSystemHealth.ts index 9a606741..277f4c01 100644 --- a/src/hooks/useSystemHealth.ts +++ b/src/hooks/useSystemHealth.ts @@ -1,15 +1,18 @@ -import { useQuery } from '@tanstack/react-query'; +import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { supabase } from '@/lib/supabaseClient'; import { handleError } from '@/lib/errorHandler'; +import { toast } from 'sonner'; -interface SystemHealthData { +export interface SystemHealthData { orphaned_images_count: number; critical_alerts_count: number; + high_alerts_count?: number; + failed_webhook_count?: number; alerts_last_24h: number; checked_at: string; } -interface SystemAlert { +export interface SystemAlert { id: string; alert_type: 'orphaned_images' | 'stale_submissions' | 'circular_dependency' | 'validation_error' | 'ban_attempt' | 'upload_timeout' | 'high_error_rate'; severity: 'low' | 'medium' | 'high' | 'critical'; @@ -101,8 +104,10 @@ export function useSystemAlerts(severity?: 'low' | 'medium' | 'high' | 'critical * Only accessible to admins */ export function useRunSystemMaintenance() { - return async () => { - try { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async () => { const { data, error } = await supabase.rpc('run_system_maintenance'); if (error) { @@ -118,12 +123,18 @@ export function useRunSystemMaintenance() { status: 'success' | 'error'; details: Record; }>; - } catch (error) { + }, + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: ['system-health'] }); + queryClient.invalidateQueries({ queryKey: ['system-alerts'] }); + toast.success('System maintenance completed successfully'); + }, + onError: (error) => { handleError(error, { action: 'Run System Maintenance', metadata: { error: String(error) } }); - throw error; - } - }; + toast.error('Failed to run system maintenance'); + }, + }); } diff --git a/src/lib/queryKeys.ts b/src/lib/queryKeys.ts index 184ebb40..7f6ed5bd 100644 --- a/src/lib/queryKeys.ts +++ b/src/lib/queryKeys.ts @@ -77,4 +77,17 @@ export const queryKeys = { lists: { items: (listId: string) => ['list-items', listId] as const, }, + + // Monitoring queries + monitoring: { + overview: () => ['monitoring', 'overview'] as const, + systemHealth: () => ['system-health'] as const, + systemAlerts: (severity?: string) => ['system-alerts', severity] as const, + rateLimitStats: (timeWindow: number) => ['rate-limit-stats', timeWindow] as const, + recentErrors: (timeWindow: number) => ['recent-errors', timeWindow] as const, + recentActivity: (timeWindow: number) => ['recent-activity', timeWindow] as const, + combinedAlerts: () => ['monitoring', 'combined-alerts'] as const, + databaseHealth: () => ['monitoring', 'database-health'] as const, + moderationHealth: () => ['monitoring', 'moderation-health'] as const, + }, } as const; diff --git a/src/pages/admin/MonitoringOverview.tsx b/src/pages/admin/MonitoringOverview.tsx new file mode 100644 index 00000000..0e03732c --- /dev/null +++ b/src/pages/admin/MonitoringOverview.tsx @@ -0,0 +1,124 @@ +import { useState } from 'react'; +import { useQueryClient } from '@tanstack/react-query'; +import { AdminLayout } from '@/components/layout/AdminLayout'; +import { RefreshButton } from '@/components/ui/refresh-button'; +import { SystemHealthStatus } from '@/components/admin/SystemHealthStatus'; +import { CriticalAlertsPanel } from '@/components/admin/CriticalAlertsPanel'; +import { MonitoringQuickStats } from '@/components/admin/MonitoringQuickStats'; +import { RecentActivityTimeline } from '@/components/admin/RecentActivityTimeline'; +import { MonitoringNavCards } from '@/components/admin/MonitoringNavCards'; +import { useSystemHealth } from '@/hooks/useSystemHealth'; +import { useCombinedAlerts } from '@/hooks/admin/useCombinedAlerts'; +import { useRecentActivity } from '@/hooks/admin/useRecentActivity'; +import { useDatabaseHealth } from '@/hooks/admin/useDatabaseHealth'; +import { useModerationHealth } from '@/hooks/admin/useModerationHealth'; +import { useRateLimitStats } from '@/hooks/useRateLimitMetrics'; +import { Switch } from '@/components/ui/switch'; +import { Label } from '@/components/ui/label'; + +export default function MonitoringOverview() { + const queryClient = useQueryClient(); + const [autoRefresh, setAutoRefresh] = useState(true); + + // Fetch all monitoring data + const systemHealth = useSystemHealth(); + const combinedAlerts = useCombinedAlerts(); + const recentActivity = useRecentActivity(3600000); // 1 hour + const dbHealth = useDatabaseHealth(); + const moderationHealth = useModerationHealth(); + const rateLimitStats = useRateLimitStats(3600000); // 1 hour + + const isLoading = + systemHealth.isLoading || + combinedAlerts.isLoading || + recentActivity.isLoading || + dbHealth.isLoading || + moderationHealth.isLoading || + rateLimitStats.isLoading; + + const handleRefresh = async () => { + await queryClient.invalidateQueries({ + queryKey: ['monitoring'], + refetchType: 'active' + }); + await queryClient.invalidateQueries({ + queryKey: ['system-health'], + refetchType: 'active' + }); + await queryClient.invalidateQueries({ + queryKey: ['system-alerts'], + refetchType: 'active' + }); + await queryClient.invalidateQueries({ + queryKey: ['rate-limit'], + refetchType: 'active' + }); + }; + + // Calculate error count for nav card (from recent activity) + const errorCount = recentActivity.data?.filter(e => e.type === 'error').length || 0; + + return ( + +
+
+
+

Monitoring Overview

+

Real-time system health, alerts, and activity monitoring

+
+
+
+ + +
+ +
+
+ + {/* System Health Status */} + + + {/* Critical Alerts */} + + + {/* Quick Stats Grid */} + + + {/* Recent Activity Timeline */} + + + {/* Quick Navigation Cards */} +
+

Detailed Dashboards

+ +
+
+
+ ); +}