Implement monitoring overview features

Add comprehensive monitoring dashboard scaffolding:
- Extend queryKeys with monitoring keys
- Create hooks: useCombinedAlerts, useRecentActivity, useDatabaseHealth, useModerationHealth
- Build UI components: SystemHealthStatus, CriticalAlertsPanel, MonitoringQuickStats, RecentActivityTimeline, MonitoringNavCards
- Create MonitoringOverview page and integrate routing (MonitoringOverview route)
- Wire AdminSidebar to include Monitoring navigation
- Introduce supporting routing and admin layout hooks/utilities
- Align with TanStack Query patterns and plan for auto-refresh and optimistic updates
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 01:33:53 +00:00
parent d94062a937
commit 99c917deaf
14 changed files with 1045 additions and 11 deletions

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