Connect to Lovable Cloud

This commit is contained in:
gpt-engineer-app[bot]
2025-11-10 14:55:58 +00:00
parent fce582e6ba
commit 236e412d7c
3 changed files with 106 additions and 14 deletions

View File

@@ -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,

View File

@@ -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({
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

View File

@@ -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({
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