Backfill Ride Data

Add edge function and UI for ride data backfill, integrating admin UI to trigger backfill, plus supporting types and wiring to backfill_ride_data function. Includes RideDataBackfill component, admin page integration, and initial server-side function scaffold for updating rides from submissions.
This commit is contained in:
gpt-engineer-app[bot]
2025-11-11 15:45:14 +00:00
parent d48e95ee7c
commit 314db65591
5 changed files with 259 additions and 0 deletions

View File

@@ -0,0 +1,110 @@
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { supabase } from '@/lib/supabaseClient';
import { Hammer, AlertCircle, CheckCircle2 } from 'lucide-react';
import { useToast } from '@/hooks/use-toast';
export function RideDataBackfill() {
const [isRunning, setIsRunning] = useState(false);
const [result, setResult] = useState<{
success: boolean;
rides_updated: number;
manufacturer_added: number;
designer_added: number;
ride_model_added: number;
} | null>(null);
const [error, setError] = useState<string | null>(null);
const { toast } = useToast();
const handleBackfill = async () => {
setIsRunning(true);
setError(null);
setResult(null);
try {
const { data, error: invokeError } = await supabase.functions.invoke(
'backfill-ride-data'
);
if (invokeError) throw invokeError;
setResult(data);
const updates: string[] = [];
if (data.manufacturer_added > 0) updates.push(`${data.manufacturer_added} manufacturers`);
if (data.designer_added > 0) updates.push(`${data.designer_added} designers`);
if (data.ride_model_added > 0) updates.push(`${data.ride_model_added} ride models`);
toast({
title: 'Backfill Complete',
description: `Updated ${data.rides_updated} rides: ${updates.join(', ')}`,
});
} catch (err: any) {
const errorMessage = err.message || 'Failed to run backfill';
setError(errorMessage);
toast({
title: 'Backfill Failed',
description: errorMessage,
variant: 'destructive',
});
} finally {
setIsRunning(false);
}
};
return (
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Hammer className="w-5 h-5" />
Ride Data Backfill
</CardTitle>
<CardDescription>
Backfill missing manufacturer, designer, and ride model data for approved rides from their submission data
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<Alert>
<AlertCircle className="h-4 w-4" />
<AlertDescription>
This tool will find rides missing manufacturer, designer, or ride model information and populate them using data from their approved submissions. Useful for fixing rides that were approved before relationship data was properly handled.
</AlertDescription>
</Alert>
{result && (
<Alert className="border-green-200 bg-green-50 dark:bg-green-950 dark:border-green-800">
<CheckCircle2 className="h-4 w-4 text-green-600 dark:text-green-400" />
<AlertDescription className="text-green-900 dark:text-green-100">
<div className="font-medium">Backfill completed successfully!</div>
<div className="mt-2 space-y-1">
<div>Rides updated: {result.rides_updated}</div>
<div>Manufacturers added: {result.manufacturer_added}</div>
<div>Designers added: {result.designer_added}</div>
<div>Ride models added: {result.ride_model_added}</div>
</div>
</AlertDescription>
</Alert>
)}
{error && (
<Alert variant="destructive">
<AlertCircle className="h-4 w-4" />
<AlertDescription>{error}</AlertDescription>
</Alert>
)}
<Button
onClick={handleBackfill}
disabled={isRunning}
className="w-full"
trackingLabel="run-ride-data-backfill"
>
<Hammer className="w-4 h-4 mr-2" />
{isRunning ? 'Running Backfill...' : 'Run Ride Data Backfill'}
</Button>
</CardContent>
</Card>
);
}

View File

@@ -6633,6 +6633,7 @@ export type Database = {
Returns: undefined Returns: undefined
} }
backfill_park_locations: { Args: never; Returns: Json } backfill_park_locations: { Args: never; Returns: Json }
backfill_ride_data: { Args: never; Returns: Json }
backfill_sort_orders: { Args: never; Returns: undefined } backfill_sort_orders: { Args: never; Returns: undefined }
block_aal1_with_mfa: { Args: never; Returns: boolean } block_aal1_with_mfa: { Args: never; Returns: boolean }
can_approve_submission_item: { can_approve_submission_item: {

View File

@@ -15,6 +15,7 @@ import { NovuMigrationUtility } from '@/components/admin/NovuMigrationUtility';
import { TestDataGenerator } from '@/components/admin/TestDataGenerator'; import { TestDataGenerator } from '@/components/admin/TestDataGenerator';
import { IntegrationTestRunner } from '@/components/admin/IntegrationTestRunner'; import { IntegrationTestRunner } from '@/components/admin/IntegrationTestRunner';
import { ParkLocationBackfill } from '@/components/admin/ParkLocationBackfill'; import { ParkLocationBackfill } from '@/components/admin/ParkLocationBackfill';
import { RideDataBackfill } from '@/components/admin/RideDataBackfill';
import { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug, AlertTriangle, Lock, TestTube, RefreshCw, Info, AlertCircle, Database } from 'lucide-react'; import { Loader2, Save, Clock, Users, Bell, Shield, Settings, Trash2, Plug, AlertTriangle, Lock, TestTube, RefreshCw, Info, AlertCircle, Database } from 'lucide-react';
import { useDocumentTitle } from '@/hooks/useDocumentTitle'; import { useDocumentTitle } from '@/hooks/useDocumentTitle';
@@ -938,6 +939,7 @@ export default function AdminSettings() {
</Card> </Card>
<ParkLocationBackfill /> <ParkLocationBackfill />
<RideDataBackfill />
</div> </div>
</TabsContent> </TabsContent>

View File

@@ -0,0 +1,54 @@
import { createEdgeFunction } from '../_shared/edgeFunctionWrapper.ts';
import { edgeLogger } from '../_shared/logger.ts';
export default createEdgeFunction(
{
name: 'backfill-ride-data',
requireAuth: true,
},
async (req, context, supabase) => {
edgeLogger.info('Starting ride data backfill', { requestId: context.requestId });
// Check if user is superuser
const { data: profile, error: profileError } = await supabase
.from('user_profiles')
.select('role')
.eq('id', context.user.id)
.single();
if (profileError || profile?.role !== 'superuser') {
edgeLogger.warn('Unauthorized backfill attempt', {
userId: context.user.id,
requestId: context.requestId
});
return new Response(
JSON.stringify({ error: 'Unauthorized: Superuser access required' }),
{ status: 403, headers: { 'Content-Type': 'application/json' } }
);
}
// Execute the backfill function
const { data, error } = await supabase.rpc('backfill_ride_data');
if (error) {
edgeLogger.error('Error running ride data backfill', {
error,
requestId: context.requestId
});
throw error;
}
edgeLogger.info('Ride data backfill completed', {
results: data,
requestId: context.requestId
});
return new Response(
JSON.stringify({
success: true,
...data,
}),
{ headers: { 'Content-Type': 'application/json' } }
);
}
);

View File

@@ -0,0 +1,92 @@
-- Function to backfill missing ride data from submission data
CREATE OR REPLACE FUNCTION backfill_ride_data()
RETURNS jsonb
LANGUAGE plpgsql
SECURITY DEFINER
SET search_path = public
AS $$
DECLARE
v_rides_updated INTEGER := 0;
v_manufacturer_added INTEGER := 0;
v_designer_added INTEGER := 0;
v_ride_model_added INTEGER := 0;
v_ride RECORD;
v_submission RECORD;
BEGIN
-- Find rides with missing manufacturer, designer, or ride_model that have approved submissions
FOR v_ride IN
SELECT DISTINCT r.id, r.name, r.slug, r.manufacturer_id, r.designer_id, r.ride_model_id
FROM rides r
WHERE r.manufacturer_id IS NULL
OR r.designer_id IS NULL
OR r.ride_model_id IS NULL
LOOP
-- Find the most recent approved submission for this ride with the missing data
SELECT
rs.manufacturer_id,
rs.designer_id,
rs.ride_model_id
INTO v_submission
FROM ride_submissions rs
JOIN content_submissions cs ON cs.id = rs.submission_id
WHERE rs.ride_id = v_ride.id
AND cs.status = 'approved'
AND (
(v_ride.manufacturer_id IS NULL AND rs.manufacturer_id IS NOT NULL) OR
(v_ride.designer_id IS NULL AND rs.designer_id IS NOT NULL) OR
(v_ride.ride_model_id IS NULL AND rs.ride_model_id IS NOT NULL)
)
ORDER BY cs.created_at DESC
LIMIT 1;
-- If we found submission data, update the ride
IF FOUND THEN
DECLARE
v_updated BOOLEAN := FALSE;
BEGIN
-- Update manufacturer if missing
IF v_ride.manufacturer_id IS NULL AND v_submission.manufacturer_id IS NOT NULL THEN
UPDATE rides
SET manufacturer_id = v_submission.manufacturer_id
WHERE id = v_ride.id;
v_manufacturer_added := v_manufacturer_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added manufacturer for ride: % (id: %)', v_ride.name, v_ride.id;
END IF;
-- Update designer if missing
IF v_ride.designer_id IS NULL AND v_submission.designer_id IS NOT NULL THEN
UPDATE rides
SET designer_id = v_submission.designer_id
WHERE id = v_ride.id;
v_designer_added := v_designer_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added designer for ride: % (id: %)', v_ride.name, v_ride.id;
END IF;
-- Update ride_model if missing
IF v_ride.ride_model_id IS NULL AND v_submission.ride_model_id IS NOT NULL THEN
UPDATE rides
SET ride_model_id = v_submission.ride_model_id
WHERE id = v_ride.id;
v_ride_model_added := v_ride_model_added + 1;
v_updated := TRUE;
RAISE NOTICE 'Added ride model for ride: % (id: %)', v_ride.name, v_ride.id;
END IF;
IF v_updated THEN
v_rides_updated := v_rides_updated + 1;
END IF;
END;
END IF;
END LOOP;
RETURN jsonb_build_object(
'success', true,
'rides_updated', v_rides_updated,
'manufacturer_added', v_manufacturer_added,
'designer_added', v_designer_added,
'ride_model_added', v_ride_model_added
);
END;
$$;