From 236e412d7ca86ac1567dc47f067b4cf3361cd4ad Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:55:58 +0000 Subject: [PATCH] Connect to Lovable Cloud --- .../upload/UppyPhotoSubmissionUpload.tsx | 48 +++++++++++++++++++ .../process-selective-approval/index.ts | 36 +++++++++++--- .../process-selective-rejection/index.ts | 36 +++++++++++--- 3 files changed, 106 insertions(+), 14 deletions(-) diff --git a/src/components/upload/UppyPhotoSubmissionUpload.tsx b/src/components/upload/UppyPhotoSubmissionUpload.tsx index c3bec629..305c3171 100644 --- a/src/components/upload/UppyPhotoSubmissionUpload.tsx +++ b/src/components/upload/UppyPhotoSubmissionUpload.tsx @@ -52,6 +52,31 @@ export function UppyPhotoSubmissionUpload({ const { user } = useAuth(); const { toast } = useToast(); + /** + * ✅ CRITICAL FIX: Cleanup orphaned Cloudflare images + * Called when DB transaction fails after successful uploads + */ + const cleanupOrphanedImages = async (imageIds: string[]) => { + if (imageIds.length === 0) return; + + logger.warn('Cleaning up orphaned images', { count: imageIds.length }); + + try { + await Promise.allSettled( + imageIds.map(id => + invokeWithTracking('upload-image', { action: 'delete', imageId: id }, user?.id) + ) + ); + logger.info('Orphaned images cleaned up', { count: imageIds.length }); + } catch (error) { + // Non-blocking cleanup - log but don't fail + logger.error('Failed to cleanup orphaned images', { + error: getErrorMessage(error), + imageIds + }); + } + }; + const handleFilesSelected = (files: File[]) => { // Convert files to photo objects with object URLs for preview const newPhotos: PhotoWithCaption[] = files.map((file, index) => ({ @@ -424,6 +449,22 @@ export function UppyPhotoSubmissionUpload({ throw photoSubmissionError || new Error("Failed to create photo submission"); } + // ✅ CRITICAL FIX: Create submission_items record for moderation queue + const { error: submissionItemError } = await supabase + .from('submission_items') + .insert({ + submission_id: submissionData.id, + item_type: 'photo', + action_type: 'create', + status: 'pending', + order_index: 0, + photo_submission_id: photoSubmissionData.id + }); + + if (submissionItemError) { + throw submissionItemError; + } + // Insert only successful photo items const photoItems = successfulPhotos.map((photo, index) => ({ photo_submission_id: photoSubmissionData.id, @@ -527,6 +568,13 @@ export function UppyPhotoSubmissionUpload({ } catch (error: unknown) { const errorMsg = sanitizeErrorMessage(error); + // ✅ CRITICAL FIX: Cleanup orphaned images on failure + if (orphanedCloudflareIds.length > 0) { + cleanupOrphanedImages(orphanedCloudflareIds).catch(() => { + // Non-blocking - log already handled in cleanupOrphanedImages + }); + } + logger.error('Photo submission failed', { error: errorMsg, photoCount: photos.length, diff --git a/supabase/functions/process-selective-approval/index.ts b/supabase/functions/process-selective-approval/index.ts index 4d64c1a7..7e2755d2 100644 --- a/supabase/functions/process-selective-approval/index.ts +++ b/supabase/functions/process-selective-approval/index.ts @@ -249,14 +249,36 @@ const handler = async (req: Request) => { ); } - // STEP 6: Register idempotency key as processing + // STEP 6: Register idempotency key as processing (atomic upsert) + // ✅ CRITICAL FIX: Use ON CONFLICT to prevent race conditions if (!existingKey) { - await supabase.from('submission_idempotency_keys').insert({ - idempotency_key: idempotencyKey, - submission_id: submissionId, - moderator_id: user.id, - status: 'processing' - }); + const { data: insertedKey, error: idempotencyError } = await supabase + .from('submission_idempotency_keys') + .insert({ + idempotency_key: idempotencyKey, + submission_id: submissionId, + moderator_id: user.id, + status: 'processing' + }) + .select() + .single(); + + // If conflict occurred, another moderator is processing + if (idempotencyError && idempotencyError.code === '23505') { + edgeLogger.warn('Idempotency key conflict - another request processing', { + requestId, + idempotencyKey, + moderatorId: user.id + }); + return new Response( + JSON.stringify({ error: 'Another moderator is processing this submission' }), + { status: 409, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + if (idempotencyError) { + throw idempotencyError; + } } // Create child span for RPC transaction diff --git a/supabase/functions/process-selective-rejection/index.ts b/supabase/functions/process-selective-rejection/index.ts index 922a459d..4c4d3e36 100644 --- a/supabase/functions/process-selective-rejection/index.ts +++ b/supabase/functions/process-selective-rejection/index.ts @@ -252,14 +252,36 @@ const handler = async (req: Request) => { ); } - // STEP 6: Register idempotency key as processing + // STEP 6: Register idempotency key as processing (atomic upsert) + // ✅ CRITICAL FIX: Use ON CONFLICT to prevent race conditions if (!existingKey) { - await supabase.from('submission_idempotency_keys').insert({ - idempotency_key: idempotencyKey, - submission_id: submissionId, - moderator_id: user.id, - status: 'processing' - }); + const { data: insertedKey, error: idempotencyError } = await supabase + .from('submission_idempotency_keys') + .insert({ + idempotency_key: idempotencyKey, + submission_id: submissionId, + moderator_id: user.id, + status: 'processing' + }) + .select() + .single(); + + // If conflict occurred, another moderator is processing + if (idempotencyError && idempotencyError.code === '23505') { + edgeLogger.warn('Idempotency key conflict - another request processing', { + requestId, + idempotencyKey, + moderatorId: user.id + }); + return new Response( + JSON.stringify({ error: 'Another moderator is processing this submission' }), + { status: 409, headers: { ...corsHeaders, 'Content-Type': 'application/json' } } + ); + } + + if (idempotencyError) { + throw idempotencyError; + } } // Create child span for RPC transaction