mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 11:31:11 -05:00
Connect to Lovable Cloud
This commit is contained in:
@@ -52,6 +52,31 @@ export function UppyPhotoSubmissionUpload({
|
|||||||
const { user } = useAuth();
|
const { user } = useAuth();
|
||||||
const { toast } = useToast();
|
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[]) => {
|
const handleFilesSelected = (files: File[]) => {
|
||||||
// Convert files to photo objects with object URLs for preview
|
// Convert files to photo objects with object URLs for preview
|
||||||
const newPhotos: PhotoWithCaption[] = files.map((file, index) => ({
|
const newPhotos: PhotoWithCaption[] = files.map((file, index) => ({
|
||||||
@@ -424,6 +449,22 @@ export function UppyPhotoSubmissionUpload({
|
|||||||
throw photoSubmissionError || new Error("Failed to create photo submission");
|
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
|
// Insert only successful photo items
|
||||||
const photoItems = successfulPhotos.map((photo, index) => ({
|
const photoItems = successfulPhotos.map((photo, index) => ({
|
||||||
photo_submission_id: photoSubmissionData.id,
|
photo_submission_id: photoSubmissionData.id,
|
||||||
@@ -527,6 +568,13 @@ export function UppyPhotoSubmissionUpload({
|
|||||||
} catch (error: unknown) {
|
} catch (error: unknown) {
|
||||||
const errorMsg = sanitizeErrorMessage(error);
|
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', {
|
logger.error('Photo submission failed', {
|
||||||
error: errorMsg,
|
error: errorMsg,
|
||||||
photoCount: photos.length,
|
photoCount: photos.length,
|
||||||
|
|||||||
@@ -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) {
|
if (!existingKey) {
|
||||||
await supabase.from('submission_idempotency_keys').insert({
|
const { data: insertedKey, error: idempotencyError } = await supabase
|
||||||
|
.from('submission_idempotency_keys')
|
||||||
|
.insert({
|
||||||
idempotency_key: idempotencyKey,
|
idempotency_key: idempotencyKey,
|
||||||
submission_id: submissionId,
|
submission_id: submissionId,
|
||||||
moderator_id: user.id,
|
moderator_id: user.id,
|
||||||
status: 'processing'
|
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
|
// Create child span for RPC transaction
|
||||||
|
|||||||
@@ -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) {
|
if (!existingKey) {
|
||||||
await supabase.from('submission_idempotency_keys').insert({
|
const { data: insertedKey, error: idempotencyError } = await supabase
|
||||||
|
.from('submission_idempotency_keys')
|
||||||
|
.insert({
|
||||||
idempotency_key: idempotencyKey,
|
idempotency_key: idempotencyKey,
|
||||||
submission_id: submissionId,
|
submission_id: submissionId,
|
||||||
moderator_id: user.id,
|
moderator_id: user.id,
|
||||||
status: 'processing'
|
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
|
// Create child span for RPC transaction
|
||||||
|
|||||||
Reference in New Issue
Block a user