From 827f0f8ea5250813eb457879f3c325dad40ac857 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 13:13:10 +0000 Subject: [PATCH] Fix: Address HMR failures and Fast Refresh incompatibility --- .../moderation/SubmissionReviewManager.tsx | 38 +++-- src/hooks/moderation/useModerationActions.ts | 23 ++- .../moderation/useModerationQueueManager.ts | 13 +- supabase/functions/novu-webhook/index.ts | 43 ++++- .../process-selective-approval/index.ts | 148 ++++++++++++++---- .../send-escalation-notification/index.ts | 30 +++- .../sync-all-moderators-to-topic/index.ts | 26 ++- .../functions/validate-email-backend/index.ts | 35 ++++- 8 files changed, 280 insertions(+), 76 deletions(-) diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index 7dd481cc..3e40c88e 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -3,6 +3,7 @@ import { useToast } from '@/hooks/use-toast'; import { useUserRole } from '@/hooks/useUserRole'; import { useAuth } from '@/hooks/useAuth'; import { handleError } from '@/lib/errorHandler'; +import { invokeWithTracking } from '@/lib/edgeFunctionTracking'; import { fetchSubmissionItems, buildDependencyTree, @@ -213,12 +214,14 @@ export function SubmissionReviewManager({ const { supabase } = await import('@/integrations/supabase/client'); // Call the edge function for backend processing - const { data, error } = await supabase.functions.invoke('process-selective-approval', { - body: { + const { data, error, requestId } = await invokeWithTracking( + 'process-selective-approval', + { itemIds: Array.from(selectedItemIds), submissionId - } - }); + }, + user?.id + ); if (error) { throw new Error(error.message || 'Failed to process approval'); @@ -228,6 +231,11 @@ export function SubmissionReviewManager({ throw new Error(data?.error || 'Approval processing failed'); } + toast({ + title: 'Items Approved', + description: `Successfully approved ${selectedItemIds.size} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`, + }); + const successCount = data.results.filter((r: any) => r.success).length; const failCount = data.results.filter((r: any) => !r.success).length; @@ -343,13 +351,15 @@ export function SubmissionReviewManager({ const { supabase } = await import('@/integrations/supabase/client'); // Call the escalation notification edge function - const { data, error } = await supabase.functions.invoke('send-escalation-notification', { - body: { + const { data, error, requestId } = await invokeWithTracking( + 'send-escalation-notification', + { submissionId, escalationReason: reason, escalatedBy: user.id - } - }); + }, + user.id + ); if (error) { console.error('Edge function error:', error); @@ -409,12 +419,14 @@ export function SubmissionReviewManager({ try { if (status === 'approved') { const { supabase } = await import('@/integrations/supabase/client'); - const { data, error } = await supabase.functions.invoke('process-selective-approval', { - body: { + const { data, error, requestId } = await invokeWithTracking( + 'process-selective-approval', + { itemIds: [itemId], submissionId - } - }); + }, + user?.id + ); if (error || !data?.success) { throw new Error(error?.message || data?.error || 'Failed to approve item'); @@ -422,7 +434,7 @@ export function SubmissionReviewManager({ toast({ title: 'Item Approved', - description: 'Successfully approved the item', + description: `Successfully approved the item${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`, }); } else { const item = items.find(i => i.id === itemId); diff --git a/src/hooks/moderation/useModerationActions.ts b/src/hooks/moderation/useModerationActions.ts index a4907be0..0f77db8f 100644 --- a/src/hooks/moderation/useModerationActions.ts +++ b/src/hooks/moderation/useModerationActions.ts @@ -4,6 +4,7 @@ import { useToast } from '@/hooks/use-toast'; import { logger } from '@/lib/logger'; import { getErrorMessage } from '@/lib/errorHandler'; import { validateMultipleItems } from '@/lib/entityValidationSchemas'; +import { invokeWithTracking } from '@/lib/edgeFunctionTracking'; import type { User } from '@supabase/supabase-js'; import type { ModerationItem } from '@/types/moderation'; @@ -182,16 +183,20 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio } } - await supabase.functions.invoke('process-selective-approval', { - body: { + const { data, error, requestId } = await invokeWithTracking( + 'process-selective-approval', + { itemIds: submissionItems.map((i) => i.id), submissionId: item.id, }, - }); + config.user?.id + ); + + if (error) throw error; toast({ title: 'Submission Approved', - description: `Successfully processed ${submissionItems.length} item(s)`, + description: `Successfully processed ${submissionItems.length} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`, }); return; } else if (action === 'rejected') { @@ -339,18 +344,20 @@ export function useModerationActions(config: ModerationActionsConfig): Moderatio return; } - const { error } = await supabase.functions.invoke('process-selective-approval', { - body: { + const { data, error, requestId } = await invokeWithTracking( + 'process-selective-approval', + { itemIds: failedItems.map((i) => i.id), submissionId: item.id, }, - }); + config.user?.id + ); if (error) throw error; toast({ title: 'Items Retried', - description: `Successfully retried ${failedItems.length} failed item(s)`, + description: `Successfully retried ${failedItems.length} failed item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`, }); logger.log(`✅ Retried ${failedItems.length} failed items for ${item.id}`); diff --git a/src/hooks/moderation/useModerationQueueManager.ts b/src/hooks/moderation/useModerationQueueManager.ts index 85974a9a..fc6cc73d 100644 --- a/src/hooks/moderation/useModerationQueueManager.ts +++ b/src/hooks/moderation/useModerationQueueManager.ts @@ -4,6 +4,7 @@ import { useToast } from "@/hooks/use-toast"; import { useAuth } from "@/hooks/useAuth"; import { logger } from "@/lib/logger"; import { getErrorMessage } from "@/lib/errorHandler"; +import { invokeWithTracking } from "@/lib/edgeFunctionTracking"; import { MODERATION_CONSTANTS } from "@/lib/moderation/constants"; import { useQueryClient } from '@tanstack/react-query'; import type { User } from "@supabase/supabase-js"; @@ -420,16 +421,20 @@ export function useModerationQueueManager(config: ModerationQueueManagerConfig): return; } - await supabase.functions.invoke("process-selective-approval", { - body: { + const { data, error, requestId } = await invokeWithTracking( + "process-selective-approval", + { itemIds: failedItems.map((i) => i.id), submissionId: item.id, }, - }); + user?.id + ); + + if (error) throw error; toast({ title: "Retry Complete", - description: `Processed ${failedItems.length} failed item(s)`, + description: `Processed ${failedItems.length} failed item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ""}`, }); // Refresh stats to update counts diff --git a/supabase/functions/novu-webhook/index.ts b/supabase/functions/novu-webhook/index.ts index bd58bdb1..c916f1c4 100644 --- a/supabase/functions/novu-webhook/index.ts +++ b/supabase/functions/novu-webhook/index.ts @@ -7,7 +7,13 @@ const corsHeaders = { 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; +// Simple request tracking +const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() }); +const endRequest = (tracking: { start: number }) => Date.now() - tracking.start; + serve(async (req) => { + const tracking = startRequest(); + if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } @@ -19,7 +25,11 @@ serve(async (req) => { const event = await req.json(); - edgeLogger.info('Received Novu webhook event', { action: 'novu_webhook', eventType: event.type }); + edgeLogger.info('Received Novu webhook event', { + action: 'novu_webhook', + eventType: event.type, + requestId: tracking.requestId + }); // Handle different webhook events switch (event.type) { @@ -36,26 +46,47 @@ serve(async (req) => { await handleNotificationFailed(supabase, event); break; default: - edgeLogger.warn('Unhandled Novu event type', { action: 'novu_webhook', eventType: event.type }); + edgeLogger.warn('Unhandled Novu event type', { + action: 'novu_webhook', + eventType: event.type, + requestId: tracking.requestId + }); } + const duration = endRequest(tracking); + return new Response( - JSON.stringify({ success: true }), + JSON.stringify({ success: true, requestId: tracking.requestId }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + }, status: 200, } ); } catch (error: any) { - edgeLogger.error('Error processing webhook', { action: 'novu_webhook', error: error?.message }); + const duration = endRequest(tracking); + edgeLogger.error('Error processing webhook', { + action: 'novu_webhook', + error: error?.message, + requestId: tracking.requestId, + duration + }); return new Response( JSON.stringify({ success: false, error: error.message, + requestId: tracking.requestId }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + }, status: 500, } ); diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index e3f7aa97..d19a2d7b 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -92,15 +92,25 @@ serve(async (req) => { if (authError || !user) { edgeLogger.error('Auth verification failed', { action: 'approval_auth', - error: authError?.message + error: authError?.message, + requestId: tracking.requestId }); + const duration = endRequest(tracking); return new Response( JSON.stringify({ error: 'Invalid authentication token.', details: authError?.message || 'No user found', - code: authError?.code + code: authError?.code, + requestId: tracking.requestId }), - { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + { + status: 401, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } @@ -128,10 +138,18 @@ serve(async (req) => { edgeLogger.info('Role check query result', { action: 'approval_role_check', userId: authenticatedUserId, rolesCount: roles?.length }); if (rolesError) { - edgeLogger.error('Role check failed', { action: 'approval_role_check', error: rolesError.message }); + edgeLogger.error('Role check failed', { action: 'approval_role_check', error: rolesError.message, requestId: tracking.requestId }); + const duration = endRequest(tracking); return new Response( - JSON.stringify({ error: 'Failed to verify user permissions.' }), - { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ error: 'Failed to verify user permissions.', requestId: tracking.requestId }), + { + status: 403, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } @@ -143,10 +161,18 @@ serve(async (req) => { edgeLogger.info('Role check result', { action: 'approval_role_result', userId: authenticatedUserId, isModerator }); if (!isModerator) { - edgeLogger.error('Insufficient permissions', { action: 'approval_role_insufficient', userId: authenticatedUserId }); + edgeLogger.error('Insufficient permissions', { action: 'approval_role_insufficient', userId: authenticatedUserId, requestId: tracking.requestId }); + const duration = endRequest(tracking); return new Response( - JSON.stringify({ error: 'Insufficient permissions. Moderator role required.' }), - { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ error: 'Insufficient permissions. Moderator role required.', requestId: tracking.requestId }), + { + status: 403, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } @@ -168,14 +194,23 @@ serve(async (req) => { // Enforce AAL2 if MFA is enrolled if (hasMFA && aal !== 'aal2') { - edgeLogger.error('AAL2 required but session is at AAL1', { action: 'approval_aal_violation', userId: authenticatedUserId }); + edgeLogger.error('AAL2 required but session is at AAL1', { action: 'approval_aal_violation', userId: authenticatedUserId, requestId: tracking.requestId }); + const duration = endRequest(tracking); return new Response( JSON.stringify({ error: 'MFA verification required', code: 'AAL2_REQUIRED', - message: 'Your role requires two-factor authentication. Please verify your identity to continue.' + message: 'Your role requires two-factor authentication. Please verify your identity to continue.', + requestId: tracking.requestId }), - { status: 403, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + { + status: 403, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } @@ -189,30 +224,58 @@ serve(async (req) => { // Validate itemIds if (!itemIds || !Array.isArray(itemIds)) { return new Response( - JSON.stringify({ error: 'itemIds is required and must be an array' }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ error: 'itemIds is required and must be an array', requestId: tracking.requestId }), + { + status: 400, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } if (itemIds.length === 0) { return new Response( - JSON.stringify({ error: 'itemIds must be a non-empty array' }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ error: 'itemIds must be a non-empty array', requestId: tracking.requestId }), + { + status: 400, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } // Validate submissionId if (!submissionId || typeof submissionId !== 'string' || submissionId.trim() === '') { return new Response( - JSON.stringify({ error: 'submissionId is required and must be a non-empty string' }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ error: 'submissionId is required and must be a non-empty string', requestId: tracking.requestId }), + { + status: 400, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } if (!uuidRegex.test(submissionId)) { return new Response( - JSON.stringify({ error: 'submissionId must be a valid UUID format' }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + JSON.stringify({ error: 'submissionId must be a valid UUID format', requestId: tracking.requestId }), + { + status: 400, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } @@ -252,15 +315,24 @@ serve(async (req) => { submissionId, itemCount: items.length, userId: authenticatedUserId, - error: errorMessage + error: errorMessage, + requestId: tracking.requestId }); return new Response( JSON.stringify({ error: 'Invalid submission structure', message: errorMessage, - details: 'The submission contains circular dependencies or missing required items' + details: 'The submission contains circular dependencies or missing required items', + requestId: tracking.requestId }), - { status: 400, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + { + status: 400, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } @@ -286,7 +358,8 @@ serve(async (req) => { edgeLogger.error('Blocking validation errors', { action: 'approval_validation_fail', itemId: item.id, - errors: validation.blockingErrors + errors: validation.blockingErrors, + requestId: tracking.requestId }); // Fail the entire batch if ANY item has blocking errors @@ -295,10 +368,15 @@ serve(async (req) => { message: 'Validation failed: Items have blocking errors that must be fixed', errors: validation.blockingErrors, failedItemId: item.id, - failedItemType: item.item_type + failedItemType: item.item_type, + requestId: tracking.requestId }), { status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } }); } @@ -524,20 +602,32 @@ serve(async (req) => { console.error('[APPROVAL] Failed to update submission status:', { error: updateError.message }); } + const duration = endRequest(tracking); + return new Response( JSON.stringify({ success: true, results: approvalResults, - submissionStatus: finalStatus + submissionStatus: finalStatus, + requestId: tracking.requestId }), - { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + { + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } + } ); } catch (error: unknown) { + const duration = endRequest(tracking); const errorMessage = error instanceof Error ? error.message : 'An unexpected error occurred'; console.error('[APPROVAL ERROR] Process failed:', { error: errorMessage, userId: authenticatedUserId, - timestamp: new Date().toISOString() + timestamp: new Date().toISOString(), + requestId: tracking.requestId, + duration }); return createErrorResponse( error, diff --git a/supabase/functions/send-escalation-notification/index.ts b/supabase/functions/send-escalation-notification/index.ts index beb07332..1db0a3e1 100644 --- a/supabase/functions/send-escalation-notification/index.ts +++ b/supabase/functions/send-escalation-notification/index.ts @@ -12,7 +12,13 @@ interface EscalationRequest { escalatedBy: string; } +// Simple request tracking +const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() }); +const endRequest = (tracking: { start: number }) => Date.now() - tracking.start; + serve(async (req) => { + const tracking = startRequest(); + if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } @@ -218,22 +224,36 @@ serve(async (req) => { console.error('Failed to update submission escalation status:', updateError); } + const duration = endRequest(tracking); + console.log('Escalation notification sent', { requestId: tracking.requestId, duration, emailId: emailResult.id }); + return new Response( JSON.stringify({ success: true, message: 'Escalation notification sent successfully', - emailId: emailResult.id + emailId: emailResult.id, + requestId: tracking.requestId }), - { headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + { headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } } ); } catch (error) { - console.error('Error in send-escalation-notification:', error); + const duration = endRequest(tracking); + console.error('Error in send-escalation-notification:', error, { requestId: tracking.requestId, duration }); return new Response( JSON.stringify({ error: error instanceof Error ? error.message : 'Unknown error occurred', - details: 'Failed to send escalation notification' + details: 'Failed to send escalation notification', + requestId: tracking.requestId }), - { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + { status: 500, headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } } ); } }); diff --git a/supabase/functions/sync-all-moderators-to-topic/index.ts b/supabase/functions/sync-all-moderators-to-topic/index.ts index 2583d45b..3a5a7f7c 100644 --- a/supabase/functions/sync-all-moderators-to-topic/index.ts +++ b/supabase/functions/sync-all-moderators-to-topic/index.ts @@ -12,7 +12,13 @@ const TOPICS = { MODERATION_REPORTS: 'moderation-reports', } as const; +// Simple request tracking +const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() }); +const endRequest = (tracking: { start: number }) => Date.now() - tracking.start; + serve(async (req) => { + const tracking = startRequest(); + if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); } @@ -91,29 +97,41 @@ serve(async (req) => { }); } - console.log('Sync completed:', results); + const duration = endRequest(tracking); + console.log('Sync completed:', results, { requestId: tracking.requestId, duration }); return new Response( JSON.stringify({ success: true, message: 'Moderator sync completed', results, + requestId: tracking.requestId }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + }, status: 200, } ); } catch (error: any) { - console.error('Error syncing moderators to topics:', error); + const duration = endRequest(tracking); + console.error('Error syncing moderators to topics:', error, { requestId: tracking.requestId, duration }); return new Response( JSON.stringify({ success: false, error: error.message, + requestId: tracking.requestId }), { - headers: { ...corsHeaders, 'Content-Type': 'application/json' }, + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + }, status: 500, } ); diff --git a/supabase/functions/validate-email-backend/index.ts b/supabase/functions/validate-email-backend/index.ts index 486c2e90..8f4d5bed 100644 --- a/supabase/functions/validate-email-backend/index.ts +++ b/supabase/functions/validate-email-backend/index.ts @@ -6,6 +6,10 @@ const corsHeaders = { 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', }; +// Simple request tracking +const startRequest = () => ({ requestId: crypto.randomUUID(), start: Date.now() }); +const endRequest = (tracking: { start: number }) => Date.now() - tracking.start; + // Common disposable email domains (subset for performance) const DISPOSABLE_DOMAINS = new Set([ 'tempmail.com', 'guerrillamail.com', '10minutemail.com', 'mailinator.com', @@ -50,6 +54,8 @@ function validateEmailFormat(email: string): EmailValidationResult { } serve(async (req) => { + const tracking = startRequest(); + // Handle CORS preflight requests if (req.method === 'OPTIONS') { return new Response(null, { headers: corsHeaders }); @@ -60,34 +66,49 @@ serve(async (req) => { if (!email || typeof email !== 'string') { return new Response( - JSON.stringify({ valid: false, reason: 'Email is required' }), + JSON.stringify({ valid: false, reason: 'Email is required', requestId: tracking.requestId }), { status: 400, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } } ); } // Validate email const result = validateEmailFormat(email.toLowerCase().trim()); + const duration = endRequest(tracking); return new Response( - JSON.stringify(result), + JSON.stringify({ ...result, requestId: tracking.requestId }), { status: 200, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } } ); } catch (error) { - console.error('Email validation error:', error); + const duration = endRequest(tracking); + console.error('Email validation error:', error, { requestId: tracking.requestId, duration }); return new Response( JSON.stringify({ valid: false, - reason: 'Failed to validate email. Please try again.' + reason: 'Failed to validate email. Please try again.', + requestId: tracking.requestId }), { status: 500, - headers: { ...corsHeaders, 'Content-Type': 'application/json' } + headers: { + ...corsHeaders, + 'Content-Type': 'application/json', + 'X-Request-ID': tracking.requestId + } } ); }