mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:51:12 -05:00
Compare commits
2 Commits
5d35fdc326
...
99c917deaf
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
99c917deaf | ||
|
|
d94062a937 |
@@ -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 {
|
||||
</AdminErrorBoundary>
|
||||
}
|
||||
/>
|
||||
<Route
|
||||
path="/admin/monitoring-overview"
|
||||
element={
|
||||
<AdminErrorBoundary section="Monitoring Overview">
|
||||
<MonitoringOverview />
|
||||
</AdminErrorBoundary>
|
||||
}
|
||||
/>
|
||||
|
||||
{/* Utility routes - lazy loaded */}
|
||||
<Route path="/force-logout" element={<ForceLogout />} />
|
||||
|
||||
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>
|
||||
);
|
||||
}
|
||||
@@ -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() {
|
||||
}
|
||||
>
|
||||
<item.icon className="w-4 h-4" />
|
||||
{!collapsed && <span>{item.title}</span>}
|
||||
{!collapsed && (
|
||||
<span className="flex items-center gap-2">
|
||||
{item.title}
|
||||
{item.badge !== undefined && (
|
||||
<Badge variant="destructive" className="text-xs h-5 px-1.5">
|
||||
{item.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
{collapsed && item.badge !== undefined && item.badge > 0 && (
|
||||
<Badge variant="destructive" className="text-xs h-5 w-5 p-0 flex items-center justify-center absolute -top-1 -right-1">
|
||||
{item.badge}
|
||||
</Badge>
|
||||
)}
|
||||
</NavLink>
|
||||
</SidebarMenuButton>
|
||||
</SidebarMenuItem>
|
||||
|
||||
49
src/hooks/admin/useCombinedAlerts.ts
Normal file
49
src/hooks/admin/useCombinedAlerts.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
43
src/hooks/admin/useDatabaseHealth.ts
Normal file
43
src/hooks/admin/useDatabaseHealth.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
36
src/hooks/admin/useModerationHealth.ts
Normal file
36
src/hooks/admin/useModerationHealth.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
77
src/hooks/admin/useRecentActivity.ts
Normal file
77
src/hooks/admin/useRecentActivity.ts
Normal file
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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<string, any>;
|
||||
}>;
|
||||
} 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');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
124
src/pages/admin/MonitoringOverview.tsx
Normal file
124
src/pages/admin/MonitoringOverview.tsx
Normal file
@@ -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 (
|
||||
<AdminLayout>
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight">Monitoring Overview</h1>
|
||||
<p className="text-muted-foreground mt-2">Real-time system health, alerts, and activity monitoring</p>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<Switch
|
||||
id="auto-refresh"
|
||||
checked={autoRefresh}
|
||||
onCheckedChange={setAutoRefresh}
|
||||
/>
|
||||
<Label htmlFor="auto-refresh" className="text-sm cursor-pointer">
|
||||
Auto-refresh
|
||||
</Label>
|
||||
</div>
|
||||
<RefreshButton
|
||||
onRefresh={handleRefresh}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Health Status */}
|
||||
<SystemHealthStatus
|
||||
systemHealth={systemHealth.data ?? undefined}
|
||||
dbHealth={dbHealth.data}
|
||||
isLoading={systemHealth.isLoading || dbHealth.isLoading}
|
||||
/>
|
||||
|
||||
{/* Critical Alerts */}
|
||||
<CriticalAlertsPanel
|
||||
alerts={combinedAlerts.data}
|
||||
isLoading={combinedAlerts.isLoading}
|
||||
/>
|
||||
|
||||
{/* Quick Stats Grid */}
|
||||
<MonitoringQuickStats
|
||||
systemHealth={systemHealth.data ?? undefined}
|
||||
rateLimitStats={rateLimitStats.data}
|
||||
moderationHealth={moderationHealth.data}
|
||||
/>
|
||||
|
||||
{/* Recent Activity Timeline */}
|
||||
<RecentActivityTimeline
|
||||
activity={recentActivity.data}
|
||||
isLoading={recentActivity.isLoading}
|
||||
/>
|
||||
|
||||
{/* Quick Navigation Cards */}
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold mb-4">Detailed Dashboards</h2>
|
||||
<MonitoringNavCards
|
||||
errorCount={errorCount}
|
||||
rateLimitCount={rateLimitStats.data?.blocked_requests}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
-- Add RLS policies for system_alerts table
|
||||
|
||||
-- SELECT policy: Moderators can view system alerts
|
||||
CREATE POLICY "Moderators can view system alerts"
|
||||
ON public.system_alerts
|
||||
FOR SELECT
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles
|
||||
WHERE user_id = auth.uid()
|
||||
AND role IN ('admin', 'moderator', 'superuser')
|
||||
)
|
||||
);
|
||||
|
||||
-- INSERT policy: System can create alerts
|
||||
CREATE POLICY "System can create alerts"
|
||||
ON public.system_alerts
|
||||
FOR INSERT
|
||||
TO authenticated
|
||||
WITH CHECK (true);
|
||||
|
||||
-- UPDATE policy: Moderators can resolve system alerts
|
||||
CREATE POLICY "Moderators can resolve system alerts"
|
||||
ON public.system_alerts
|
||||
FOR UPDATE
|
||||
TO authenticated
|
||||
USING (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles
|
||||
WHERE user_id = auth.uid()
|
||||
AND role IN ('admin', 'moderator', 'superuser')
|
||||
)
|
||||
)
|
||||
WITH CHECK (
|
||||
EXISTS (
|
||||
SELECT 1 FROM public.user_roles
|
||||
WHERE user_id = auth.uid()
|
||||
AND role IN ('admin', 'moderator', 'superuser')
|
||||
)
|
||||
);
|
||||
|
||||
-- Grant permissions to authenticated users
|
||||
GRANT SELECT, INSERT, UPDATE ON public.system_alerts TO authenticated;
|
||||
Reference in New Issue
Block a user