mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 16:31:12 -05:00
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:
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');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user