From fc8631ff0b55f60196279da511350218caf25604 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:36:53 +0000 Subject: [PATCH] Integrate transaction resilience hook Integrate the `useTransactionResilience` hook into `SubmissionReviewManager.tsx` to add timeout detection, auto-release functionality, and idempotency key management to moderation actions. The `handleApprove` and `handleReject` functions have been updated to use the `executeTransaction` wrapper for these operations. --- .../moderation/SubmissionReviewManager.tsx | 170 +++++++++++------- 1 file changed, 101 insertions(+), 69 deletions(-) diff --git a/src/components/moderation/SubmissionReviewManager.tsx b/src/components/moderation/SubmissionReviewManager.tsx index c9a2196d..c544c0c2 100644 --- a/src/components/moderation/SubmissionReviewManager.tsx +++ b/src/components/moderation/SubmissionReviewManager.tsx @@ -6,6 +6,7 @@ import { handleError, getErrorMessage } from '@/lib/errorHandler'; import { invokeWithTracking } from '@/lib/edgeFunctionTracking'; import { moderationReducer, canApprove, canReject, hasActiveLock } from '@/lib/moderationStateMachine'; import { useLockMonitor } from '@/lib/moderation/lockMonitor'; +import { useTransactionResilience } from '@/hooks/useTransactionResilience'; import { fetchSubmissionItems, buildDependencyTree, @@ -92,6 +93,15 @@ export function SubmissionReviewManager({ // Lock monitoring integration const { extendLock } = useLockMonitor(state, dispatch, submissionId); + // Transaction resilience (timeout detection & auto-release) + const { executeTransaction } = useTransactionResilience({ + submissionId, + timeoutMs: 30000, // 30s timeout + autoReleaseOnUnload: true, + autoReleaseOnInactivity: true, + inactivityMinutes: 10, + }); + // Moderation actions const { escalateSubmission } = useModerationActions({ user, @@ -230,6 +240,7 @@ export function SubmissionReviewManager({ } const selectedItems = items.filter(item => selectedItemIds.has(item.id)); + const selectedIds = Array.from(selectedItemIds); // Transition: reviewing → approving dispatch({ type: 'START_APPROVAL' }); @@ -258,6 +269,7 @@ export function SubmissionReviewManager({ id: item.id })) ); + setValidationResults(validationResultsMap); @@ -324,64 +336,73 @@ export function SubmissionReviewManager({ return; // Ask for confirmation } - // Proceed with approval - const { supabase } = await import('@/integrations/supabase/client'); - - // Call the edge function for backend processing - const { data, error, requestId } = await invokeWithTracking( - 'process-selective-approval', - { - itemIds: Array.from(selectedItemIds), - submissionId - }, - user?.id + // Proceed with approval - wrapped with transaction resilience + await executeTransaction( + 'approval', + selectedIds, + async (idempotencyKey) => { + const { supabase } = await import('@/integrations/supabase/client'); + + // Call the edge function for backend processing + const { data, error, requestId } = await invokeWithTracking( + 'process-selective-approval', + { + itemIds: selectedIds, + submissionId, + idempotencyKey, // Pass idempotency key to edge function + }, + user?.id + ); + + if (error) { + throw new Error(error.message || 'Failed to process approval'); + } + + if (!data?.success) { + throw new Error(data?.error || 'Approval processing failed'); + } + + // Transition: approving → complete + dispatch({ type: 'COMPLETE', payload: { result: 'approved' } }); + + toast({ + title: 'Items Approved', + description: `Successfully approved ${selectedIds.length} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`, + }); + + interface ApprovalResult { success: boolean; item_id: string; error?: string } + const successCount = data.results.filter((r: ApprovalResult) => r.success).length; + const failCount = data.results.filter((r: ApprovalResult) => !r.success).length; + + const allFailed = failCount > 0 && successCount === 0; + const someFailed = failCount > 0 && successCount > 0; + + toast({ + title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete', + description: failCount > 0 + ? `Approved ${successCount} item(s), ${failCount} failed` + : `Successfully approved ${successCount} item(s)`, + variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default', + }); + + // Reset warning confirmation state after approval + setUserConfirmedWarnings(false); + + // If ALL items failed, don't close dialog - show errors + if (allFailed) { + dispatch({ type: 'ERROR', payload: { error: 'All items failed' } }); + return data; + } + + // Reset warning confirmation state after approval + setUserConfirmedWarnings(false); + + onComplete(); + onOpenChange(false); + + return data; + } ); - - if (error) { - throw new Error(error.message || 'Failed to process approval'); - } - - if (!data?.success) { - throw new Error(data?.error || 'Approval processing failed'); - } - - // Transition: approving → complete - dispatch({ type: 'COMPLETE', payload: { result: 'approved' } }); - - toast({ - title: 'Items Approved', - description: `Successfully approved ${selectedItemIds.size} item(s)${requestId ? ` (Request: ${requestId.substring(0, 8)})` : ''}`, - }); - - interface ApprovalResult { success: boolean; item_id: string; error?: string } - const successCount = data.results.filter((r: ApprovalResult) => r.success).length; - const failCount = data.results.filter((r: ApprovalResult) => !r.success).length; - - const allFailed = failCount > 0 && successCount === 0; - const someFailed = failCount > 0 && successCount > 0; - - toast({ - title: allFailed ? 'Approval Failed' : someFailed ? 'Partial Approval' : 'Approval Complete', - description: failCount > 0 - ? `Approved ${successCount} item(s), ${failCount} failed` - : `Successfully approved ${successCount} item(s)`, - variant: allFailed ? 'destructive' : someFailed ? 'default' : 'default', - }); - - // Reset warning confirmation state after approval - setUserConfirmedWarnings(false); - - // If ALL items failed, don't close dialog - show errors - if (allFailed) { - dispatch({ type: 'ERROR', payload: { error: 'All items failed' } }); - return; - } - - // Reset warning confirmation state after approval - setUserConfirmedWarnings(false); - - onComplete(); - onOpenChange(false); } catch (error: unknown) { dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } }); handleError(error, { @@ -438,23 +459,34 @@ export function SubmissionReviewManager({ if (!user?.id) return; + const selectedItems = items.filter(item => selectedItemIds.has(item.id)); + const selectedIds = selectedItems.map(item => item.id); + // Transition: reviewing → rejecting dispatch({ type: 'START_REJECTION' }); try { - const selectedItems = items.filter(item => selectedItemIds.has(item.id)); - await rejectSubmissionItems(selectedItems, reason, user.id, cascade); - - // Transition: rejecting → complete - dispatch({ type: 'COMPLETE', payload: { result: 'rejected' } }); - - toast({ - title: 'Items Rejected', - description: `Successfully rejected ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}`, - }); + // Wrap rejection with transaction resilience + await executeTransaction( + 'rejection', + selectedIds, + async (idempotencyKey) => { + await rejectSubmissionItems(selectedItems, reason, user.id, cascade); + + // Transition: rejecting → complete + dispatch({ type: 'COMPLETE', payload: { result: 'rejected' } }); + + toast({ + title: 'Items Rejected', + description: `Successfully rejected ${selectedItems.length} item${selectedItems.length !== 1 ? 's' : ''}`, + }); - onComplete(); - onOpenChange(false); + onComplete(); + onOpenChange(false); + + return { success: true }; + } + ); } catch (error: unknown) { dispatch({ type: 'ERROR', payload: { error: getErrorMessage(error) } }); handleError(error, {