Compare commits

...

2 Commits

Author SHA1 Message Date
gpt-engineer-app[bot]
99c917deaf 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
2025-11-11 01:33:53 +00:00
gpt-engineer-app[bot]
d94062a937 Connect to Lovable Cloud
The migration completed successfully to enable moderation actions:
- Added SELECT, INSERT, and UPDATE RLS policies for system_alerts
- Grants issued to authenticated users
- Enables viewing, creating, and resolving Pipeline Health alerts via UI
- Resolves the previous issue where Resolve did nothing by lacking permissions
2025-11-11 01:26:06 +00:00
15 changed files with 1089 additions and 11 deletions

View File

@@ -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 />} />

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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>

View 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,
});
}

View 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,
});
}

View 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,
});
}

View 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,
});
}

View File

@@ -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');
},
});
}

View File

@@ -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;

View 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>
);
}

View File

@@ -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;