mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:31:13 -05:00
Connect to Lovable Cloud
Fix security definer view issue by enabling security_invoker on grouped_alerts_view and implement grouped alerts system with new keys, hooks, components, and monitoring overview integration. Added migration to adjust view, updated query keys, created useGroupedAlerts, useAlertGroupActions, GroupedAlertsPanel, and updated MonitoringOverview to include grouped alerts.
This commit is contained in:
237
src/components/admin/GroupedAlertsPanel.tsx
Normal file
237
src/components/admin/GroupedAlertsPanel.tsx
Normal file
@@ -0,0 +1,237 @@
|
||||
import { useState } from 'react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { AlertCircle, AlertTriangle, Info, ChevronDown, ChevronUp, Clock, Zap, RefreshCw } from 'lucide-react';
|
||||
import { formatDistanceToNow } from 'date-fns';
|
||||
import type { GroupedAlert } from '@/hooks/admin/useGroupedAlerts';
|
||||
import { useResolveAlertGroup, useSnoozeAlertGroup } from '@/hooks/admin/useAlertGroupActions';
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
|
||||
interface GroupedAlertsPanelProps {
|
||||
alerts?: GroupedAlert[];
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const SEVERITY_CONFIG = {
|
||||
critical: { color: 'text-destructive', icon: AlertCircle, label: 'Critical', badge: 'bg-destructive/10 text-destructive' },
|
||||
high: { color: 'text-orange-500', icon: AlertTriangle, label: 'High', badge: 'bg-orange-500/10 text-orange-500' },
|
||||
medium: { color: 'text-yellow-500', icon: AlertTriangle, label: 'Medium', badge: 'bg-yellow-500/10 text-yellow-500' },
|
||||
low: { color: 'text-blue-500', icon: Info, label: 'Low', badge: 'bg-blue-500/10 text-blue-500' },
|
||||
};
|
||||
|
||||
export function GroupedAlertsPanel({ alerts, isLoading }: GroupedAlertsPanelProps) {
|
||||
const [expandedGroups, setExpandedGroups] = useState<Set<string>>(new Set());
|
||||
const resolveGroup = useResolveAlertGroup();
|
||||
const snoozeGroup = useSnoozeAlertGroup();
|
||||
|
||||
// Filter out snoozed alerts
|
||||
const snoozedAlerts = JSON.parse(localStorage.getItem('snoozed_alerts') || '{}');
|
||||
const visibleAlerts = alerts?.filter(alert => {
|
||||
const snoozeUntil = snoozedAlerts[alert.group_key];
|
||||
return !snoozeUntil || Date.now() > snoozeUntil;
|
||||
});
|
||||
|
||||
const handleResolveGroup = (alert: GroupedAlert) => {
|
||||
resolveGroup.mutate({
|
||||
alertIds: alert.alert_ids,
|
||||
source: alert.source,
|
||||
});
|
||||
};
|
||||
|
||||
const handleSnooze = (alert: GroupedAlert, durationMs: number) => {
|
||||
snoozeGroup.mutate({
|
||||
groupKey: alert.group_key,
|
||||
duration: durationMs,
|
||||
});
|
||||
};
|
||||
|
||||
const toggleExpanded = (groupKey: string) => {
|
||||
setExpandedGroups(prev => {
|
||||
const next = new Set(prev);
|
||||
if (next.has(groupKey)) {
|
||||
next.delete(groupKey);
|
||||
} else {
|
||||
next.add(groupKey);
|
||||
}
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Critical Alerts</CardTitle>
|
||||
<CardDescription>Loading alerts...</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex items-center justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary"></div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
if (!visibleAlerts || visibleAlerts.length === 0) {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>Critical Alerts</CardTitle>
|
||||
<CardDescription>All systems operational</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="flex flex-col items-center justify-center py-8 text-muted-foreground">
|
||||
<AlertCircle className="h-12 w-12 mb-2 opacity-50" />
|
||||
<p>No active alerts</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
const totalAlerts = visibleAlerts.reduce((sum, alert) => sum + alert.unresolved_count, 0);
|
||||
const recurringCount = visibleAlerts.filter(a => a.is_recurring).length;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between">
|
||||
<span>Critical Alerts</span>
|
||||
<span className="text-sm font-normal text-muted-foreground">
|
||||
{visibleAlerts.length} {visibleAlerts.length === 1 ? 'group' : 'groups'} • {totalAlerts} total alerts
|
||||
{recurringCount > 0 && ` • ${recurringCount} recurring`}
|
||||
</span>
|
||||
</CardTitle>
|
||||
<CardDescription>Grouped by type to reduce alert fatigue</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-3">
|
||||
{visibleAlerts.map(alert => {
|
||||
const config = SEVERITY_CONFIG[alert.severity];
|
||||
const Icon = config.icon;
|
||||
const isExpanded = expandedGroups.has(alert.group_key);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={alert.group_key}
|
||||
className="border rounded-lg p-4 space-y-2 bg-card hover:bg-accent/5 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex items-start gap-3 flex-1">
|
||||
<Icon className={`h-5 w-5 mt-0.5 ${config.color}`} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 flex-wrap mb-1">
|
||||
<span className={`text-xs font-medium px-2 py-0.5 rounded ${config.badge}`}>
|
||||
{config.label}
|
||||
</span>
|
||||
<span className="text-xs px-2 py-0.5 rounded bg-muted text-muted-foreground">
|
||||
{alert.source === 'system' ? 'System' : 'Rate Limit'}
|
||||
</span>
|
||||
{alert.is_active && (
|
||||
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded bg-green-500/10 text-green-600">
|
||||
<Zap className="h-3 w-3" />
|
||||
Active
|
||||
</span>
|
||||
)}
|
||||
{alert.is_recurring && (
|
||||
<span className="flex items-center gap-1 text-xs px-2 py-0.5 rounded bg-amber-500/10 text-amber-600">
|
||||
<RefreshCw className="h-3 w-3" />
|
||||
Recurring
|
||||
</span>
|
||||
)}
|
||||
<span className="text-xs font-semibold px-2 py-0.5 rounded bg-primary/10 text-primary">
|
||||
{alert.unresolved_count} {alert.unresolved_count === 1 ? 'alert' : 'alerts'}
|
||||
</span>
|
||||
</div>
|
||||
<p className="text-sm font-medium">
|
||||
{alert.alert_type || alert.metric_type || 'Alert'}
|
||||
{alert.function_name && <span className="text-muted-foreground"> • {alert.function_name}</span>}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground line-clamp-2">
|
||||
{alert.messages[0]}
|
||||
</p>
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-muted-foreground">
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
First: {formatDistanceToNow(new Date(alert.first_seen), { addSuffix: true })}
|
||||
</span>
|
||||
<span className="flex items-center gap-1">
|
||||
<Clock className="h-3 w-3" />
|
||||
Last: {formatDistanceToNow(new Date(alert.last_seen), { addSuffix: true })}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{alert.alert_count > 1 && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => toggleExpanded(alert.group_key)}
|
||||
>
|
||||
{isExpanded ? (
|
||||
<>
|
||||
<ChevronUp className="h-4 w-4 mr-1" />
|
||||
Hide
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<ChevronDown className="h-4 w-4 mr-1" />
|
||||
Show all {alert.alert_count}
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
Snooze
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem onClick={() => handleSnooze(alert, 3600000)}>
|
||||
1 hour
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleSnooze(alert, 14400000)}>
|
||||
4 hours
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleSnooze(alert, 86400000)}>
|
||||
24 hours
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
variant="default"
|
||||
size="sm"
|
||||
onClick={() => handleResolveGroup(alert)}
|
||||
disabled={resolveGroup.isPending}
|
||||
>
|
||||
Resolve All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{isExpanded && alert.messages.length > 1 && (
|
||||
<div className="mt-3 pt-3 border-t space-y-2">
|
||||
<p className="text-xs font-medium text-muted-foreground">All messages in this group:</p>
|
||||
<div className="space-y-1 max-h-64 overflow-y-auto">
|
||||
{alert.messages.map((message, idx) => (
|
||||
<div key={idx} className="text-xs p-2 rounded bg-muted/50">
|
||||
{message}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
110
src/hooks/admin/useAlertGroupActions.ts
Normal file
110
src/hooks/admin/useAlertGroupActions.ts
Normal file
@@ -0,0 +1,110 @@
|
||||
import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
import { toast } from 'sonner';
|
||||
import type { GroupedAlert } from './useGroupedAlerts';
|
||||
|
||||
export function useResolveAlertGroup() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
alertIds,
|
||||
source
|
||||
}: {
|
||||
alertIds: string[];
|
||||
source: 'system' | 'rate_limit';
|
||||
}) => {
|
||||
const table = source === 'system' ? 'system_alerts' : 'rate_limit_alerts';
|
||||
const { error } = await supabase
|
||||
.from(table)
|
||||
.update({ resolved_at: new Date().toISOString() })
|
||||
.in('id', alertIds);
|
||||
|
||||
if (error) throw error;
|
||||
return { count: alertIds.length };
|
||||
},
|
||||
onMutate: async ({ alertIds }) => {
|
||||
// Cancel any outgoing refetches
|
||||
await queryClient.cancelQueries({
|
||||
queryKey: queryKeys.monitoring.groupedAlerts()
|
||||
});
|
||||
|
||||
const previousData = queryClient.getQueryData(
|
||||
queryKeys.monitoring.groupedAlerts()
|
||||
);
|
||||
|
||||
// Optimistically update to the new value
|
||||
queryClient.setQueryData(
|
||||
queryKeys.monitoring.groupedAlerts(),
|
||||
(old: GroupedAlert[] | undefined) => {
|
||||
if (!old) return old;
|
||||
return old.map(alert => {
|
||||
const hasMatchingIds = alert.alert_ids.some(id =>
|
||||
alertIds.includes(id)
|
||||
);
|
||||
if (hasMatchingIds) {
|
||||
return {
|
||||
...alert,
|
||||
unresolved_count: 0,
|
||||
has_resolved: true,
|
||||
};
|
||||
}
|
||||
return alert;
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
return { previousData };
|
||||
},
|
||||
onSuccess: (data) => {
|
||||
toast.success(`Resolved ${data.count} alert${data.count > 1 ? 's' : ''}`);
|
||||
},
|
||||
onError: (error, variables, context) => {
|
||||
// Rollback on error
|
||||
if (context?.previousData) {
|
||||
queryClient.setQueryData(
|
||||
queryKeys.monitoring.groupedAlerts(),
|
||||
context.previousData
|
||||
);
|
||||
}
|
||||
toast.error('Failed to resolve alerts');
|
||||
console.error('Error resolving alert group:', error);
|
||||
},
|
||||
onSettled: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.monitoring.groupedAlerts()
|
||||
});
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.monitoring.combinedAlerts()
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useSnoozeAlertGroup() {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({
|
||||
groupKey,
|
||||
duration
|
||||
}: {
|
||||
groupKey: string;
|
||||
duration: number;
|
||||
}) => {
|
||||
const snoozedAlerts = JSON.parse(
|
||||
localStorage.getItem('snoozed_alerts') || '{}'
|
||||
);
|
||||
snoozedAlerts[groupKey] = Date.now() + duration;
|
||||
localStorage.setItem('snoozed_alerts', JSON.stringify(snoozedAlerts));
|
||||
return { groupKey, until: snoozedAlerts[groupKey] };
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.monitoring.groupedAlerts()
|
||||
});
|
||||
toast.success('Alert group snoozed');
|
||||
},
|
||||
});
|
||||
}
|
||||
90
src/hooks/admin/useGroupedAlerts.ts
Normal file
90
src/hooks/admin/useGroupedAlerts.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { supabase } from '@/lib/supabaseClient';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
|
||||
export interface GroupedAlert {
|
||||
group_key: string;
|
||||
alert_type?: string;
|
||||
severity: 'critical' | 'high' | 'medium' | 'low';
|
||||
source: 'system' | 'rate_limit';
|
||||
function_name?: string;
|
||||
metric_type?: string;
|
||||
alert_count: number;
|
||||
unresolved_count: number;
|
||||
first_seen: string;
|
||||
last_seen: string;
|
||||
alert_ids: string[];
|
||||
messages: string[];
|
||||
has_resolved: boolean;
|
||||
is_recurring: boolean;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
interface GroupedAlertsOptions {
|
||||
includeResolved?: boolean;
|
||||
minCount?: number;
|
||||
severity?: 'critical' | 'high' | 'medium' | 'low';
|
||||
}
|
||||
|
||||
export function useGroupedAlerts(options?: GroupedAlertsOptions) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.monitoring.groupedAlerts(options),
|
||||
queryFn: async () => {
|
||||
let query = supabase
|
||||
.from('grouped_alerts_view')
|
||||
.select('*')
|
||||
.order('last_seen', { ascending: false });
|
||||
|
||||
if (!options?.includeResolved) {
|
||||
query = query.gt('unresolved_count', 0);
|
||||
}
|
||||
|
||||
if (options?.minCount) {
|
||||
query = query.gte('alert_count', options.minCount);
|
||||
}
|
||||
|
||||
if (options?.severity) {
|
||||
query = query.eq('severity', options.severity);
|
||||
}
|
||||
|
||||
const { data, error } = await query;
|
||||
if (error) throw error;
|
||||
|
||||
return (data || []).map(alert => ({
|
||||
...alert,
|
||||
is_recurring: (alert.alert_count ?? 0) > 3,
|
||||
is_active: new Date(alert.last_seen ?? new Date()).getTime() > Date.now() - 3600000,
|
||||
})) as GroupedAlert[];
|
||||
},
|
||||
staleTime: 15000,
|
||||
refetchInterval: 30000,
|
||||
});
|
||||
}
|
||||
|
||||
export function useAlertGroupDetails(groupKey: string, source: 'system' | 'rate_limit', alertIds: string[]) {
|
||||
return useQuery({
|
||||
queryKey: queryKeys.monitoring.alertGroupDetails(groupKey),
|
||||
queryFn: async () => {
|
||||
if (source === 'system') {
|
||||
const { data, error } = await supabase
|
||||
.from('system_alerts')
|
||||
.select('*')
|
||||
.in('id', alertIds)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
} else {
|
||||
const { data, error } = await supabase
|
||||
.from('rate_limit_alerts')
|
||||
.select('*')
|
||||
.in('id', alertIds)
|
||||
.order('created_at', { ascending: false });
|
||||
|
||||
if (error) throw error;
|
||||
return data || [];
|
||||
}
|
||||
},
|
||||
enabled: alertIds.length > 0,
|
||||
});
|
||||
}
|
||||
@@ -89,5 +89,8 @@ export const queryKeys = {
|
||||
combinedAlerts: () => ['monitoring', 'combined-alerts'] as const,
|
||||
databaseHealth: () => ['monitoring', 'database-health'] as const,
|
||||
moderationHealth: () => ['monitoring', 'moderation-health'] as const,
|
||||
groupedAlerts: (options?: { includeResolved?: boolean; minCount?: number; severity?: string }) =>
|
||||
['monitoring', 'grouped-alerts', options] as const,
|
||||
alertGroupDetails: (groupKey: string) => ['monitoring', 'alert-group-details', groupKey] as const,
|
||||
},
|
||||
} as const;
|
||||
|
||||
@@ -3,16 +3,17 @@ 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 { GroupedAlertsPanel } from '@/components/admin/GroupedAlertsPanel';
|
||||
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 { useGroupedAlerts } from '@/hooks/admin/useGroupedAlerts';
|
||||
import { useRecentActivity } from '@/hooks/admin/useRecentActivity';
|
||||
import { useDatabaseHealth } from '@/hooks/admin/useDatabaseHealth';
|
||||
import { useModerationHealth } from '@/hooks/admin/useModerationHealth';
|
||||
import { useRateLimitStats } from '@/hooks/useRateLimitMetrics';
|
||||
import { queryKeys } from '@/lib/queryKeys';
|
||||
import { Switch } from '@/components/ui/switch';
|
||||
import { Label } from '@/components/ui/label';
|
||||
|
||||
@@ -22,7 +23,7 @@ export default function MonitoringOverview() {
|
||||
|
||||
// Fetch all monitoring data
|
||||
const systemHealth = useSystemHealth();
|
||||
const combinedAlerts = useCombinedAlerts();
|
||||
const groupedAlerts = useGroupedAlerts({ includeResolved: false });
|
||||
const recentActivity = useRecentActivity(3600000); // 1 hour
|
||||
const dbHealth = useDatabaseHealth();
|
||||
const moderationHealth = useModerationHealth();
|
||||
@@ -30,7 +31,7 @@ export default function MonitoringOverview() {
|
||||
|
||||
const isLoading =
|
||||
systemHealth.isLoading ||
|
||||
combinedAlerts.isLoading ||
|
||||
groupedAlerts.isLoading ||
|
||||
recentActivity.isLoading ||
|
||||
dbHealth.isLoading ||
|
||||
moderationHealth.isLoading ||
|
||||
@@ -53,10 +54,18 @@ export default function MonitoringOverview() {
|
||||
queryKey: ['rate-limit'],
|
||||
refetchType: 'active'
|
||||
});
|
||||
await queryClient.invalidateQueries({
|
||||
queryKey: queryKeys.monitoring.groupedAlerts(),
|
||||
refetchType: 'active'
|
||||
});
|
||||
};
|
||||
|
||||
// Calculate error count for nav card (from recent activity)
|
||||
const errorCount = recentActivity.data?.filter(e => e.type === 'error').length || 0;
|
||||
|
||||
// Calculate stats from grouped alerts
|
||||
const totalGroupedAlerts = groupedAlerts.data?.reduce((sum, g) => sum + g.unresolved_count, 0) || 0;
|
||||
const recurringIssues = groupedAlerts.data?.filter(g => g.is_recurring).length || 0;
|
||||
|
||||
return (
|
||||
<AdminLayout>
|
||||
@@ -91,10 +100,10 @@ export default function MonitoringOverview() {
|
||||
isLoading={systemHealth.isLoading || dbHealth.isLoading}
|
||||
/>
|
||||
|
||||
{/* Critical Alerts */}
|
||||
<CriticalAlertsPanel
|
||||
alerts={combinedAlerts.data}
|
||||
isLoading={combinedAlerts.isLoading}
|
||||
{/* Critical Alerts - Now Grouped */}
|
||||
<GroupedAlertsPanel
|
||||
alerts={groupedAlerts.data}
|
||||
isLoading={groupedAlerts.isLoading}
|
||||
/>
|
||||
|
||||
{/* Quick Stats Grid */}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
-- Fix security definer view issue by enabling security_invoker
|
||||
-- This ensures the view respects RLS policies and runs with the querying user's permissions
|
||||
DROP VIEW IF EXISTS grouped_alerts_view;
|
||||
|
||||
CREATE OR REPLACE VIEW grouped_alerts_view
|
||||
WITH (security_invoker=on)
|
||||
AS
|
||||
WITH system_alerts_grouped AS (
|
||||
SELECT
|
||||
alert_type AS group_key,
|
||||
alert_type,
|
||||
severity,
|
||||
'system'::text AS source,
|
||||
NULL::text AS function_name,
|
||||
NULL::text AS metric_type,
|
||||
COUNT(*) AS alert_count,
|
||||
MIN(created_at) AS first_seen,
|
||||
MAX(created_at) AS last_seen,
|
||||
ARRAY_AGG(id::text ORDER BY created_at DESC) AS alert_ids,
|
||||
ARRAY_AGG(message ORDER BY created_at DESC) AS messages,
|
||||
BOOL_OR(resolved_at IS NOT NULL) AS has_resolved,
|
||||
COUNT(*) FILTER (WHERE resolved_at IS NULL) AS unresolved_count
|
||||
FROM system_alerts
|
||||
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||
GROUP BY alert_type, severity
|
||||
),
|
||||
rate_limit_alerts_grouped AS (
|
||||
SELECT
|
||||
CONCAT(metric_type, ':', COALESCE(function_name, 'global')) AS group_key,
|
||||
NULL::text AS alert_type,
|
||||
'high'::text AS severity,
|
||||
'rate_limit'::text AS source,
|
||||
function_name,
|
||||
metric_type,
|
||||
COUNT(*) AS alert_count,
|
||||
MIN(created_at) AS first_seen,
|
||||
MAX(created_at) AS last_seen,
|
||||
ARRAY_AGG(id::text ORDER BY created_at DESC) AS alert_ids,
|
||||
ARRAY_AGG(alert_message ORDER BY created_at DESC) AS messages,
|
||||
BOOL_OR(resolved_at IS NOT NULL) AS has_resolved,
|
||||
COUNT(*) FILTER (WHERE resolved_at IS NULL) AS unresolved_count
|
||||
FROM rate_limit_alerts
|
||||
WHERE created_at > NOW() - INTERVAL '7 days'
|
||||
GROUP BY metric_type, function_name
|
||||
)
|
||||
SELECT * FROM system_alerts_grouped
|
||||
UNION ALL
|
||||
SELECT * FROM rate_limit_alerts_grouped;
|
||||
|
||||
-- Grant access to authenticated users
|
||||
GRANT SELECT ON grouped_alerts_view TO authenticated;
|
||||
Reference in New Issue
Block a user