Graphical PDS migrator for AT Protocol
1import { useEffect, useState } from "preact/hooks"; 2import { MigrationStep } from "../../components/MigrationStep.tsx"; 3import { 4 parseApiResponse, 5 StepCommonProps, 6 verifyMigrationStep, 7} from "../../lib/migration-types.ts"; 8 9interface DataMigrationStepProps extends StepCommonProps { 10 isActive: boolean; 11} 12 13export default function DataMigrationStep({ 14 credentials: _credentials, 15 onStepComplete, 16 onStepError, 17 isActive, 18}: DataMigrationStepProps) { 19 const [status, setStatus] = useState< 20 "pending" | "in-progress" | "verifying" | "completed" | "error" 21 >("pending"); 22 const [error, setError] = useState<string>(); 23 const [retryCount, setRetryCount] = useState(0); 24 const [showContinueAnyway, setShowContinueAnyway] = useState(false); 25 26 useEffect(() => { 27 if (isActive && status === "pending") { 28 startDataMigration(); 29 } 30 }, [isActive]); 31 32 const startDataMigration = async () => { 33 setStatus("in-progress"); 34 setError(undefined); 35 36 try { 37 // Step 1: Migrate Repo 38 const repoRes = await fetch("/api/migrate/data/repo", { 39 method: "POST", 40 headers: { "Content-Type": "application/json" }, 41 }); 42 43 const repoText = await repoRes.text(); 44 45 if (!repoRes.ok) { 46 const parsed = parseApiResponse(repoText); 47 throw new Error(parsed.message || "Failed to migrate repo"); 48 } 49 50 // Step 2: Migrate Blobs 51 const blobsRes = await fetch("/api/migrate/data/blobs", { 52 method: "POST", 53 headers: { "Content-Type": "application/json" }, 54 }); 55 56 const blobsText = await blobsRes.text(); 57 58 if (!blobsRes.ok) { 59 const parsed = parseApiResponse(blobsText); 60 throw new Error(parsed.message || "Failed to migrate blobs"); 61 } 62 63 // Step 3: Migrate Preferences 64 const prefsRes = await fetch("/api/migrate/data/prefs", { 65 method: "POST", 66 headers: { "Content-Type": "application/json" }, 67 }); 68 69 const prefsText = await prefsRes.text(); 70 71 if (!prefsRes.ok) { 72 const parsed = parseApiResponse(prefsText); 73 throw new Error(parsed.message || "Failed to migrate preferences"); 74 } 75 76 // Verify the data migration 77 await verifyDataMigration(); 78 } catch (error) { 79 const errorMessage = error instanceof Error 80 ? error.message 81 : String(error); 82 setError(errorMessage); 83 setStatus("error"); 84 onStepError(errorMessage); 85 } 86 }; 87 88 const verifyDataMigration = async () => { 89 setStatus("verifying"); 90 91 try { 92 const result = await verifyMigrationStep(2); 93 94 if (result.ready) { 95 setStatus("completed"); 96 setRetryCount(0); 97 setShowContinueAnyway(false); 98 onStepComplete(); 99 } else { 100 const statusDetails = { 101 repoCommit: result.repoCommit, 102 repoRev: result.repoRev, 103 repoBlocks: result.repoBlocks, 104 expectedRecords: result.expectedRecords, 105 indexedRecords: result.indexedRecords, 106 privateStateValues: result.privateStateValues, 107 expectedBlobs: result.expectedBlobs, 108 importedBlobs: result.importedBlobs, 109 }; 110 const errorMessage = `${ 111 result.reason || "Verification failed" 112 }\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`; 113 114 setRetryCount((prev) => prev + 1); 115 if (retryCount >= 1) { 116 setShowContinueAnyway(true); 117 } 118 119 setError(errorMessage); 120 setStatus("error"); 121 onStepError(errorMessage, true); 122 } 123 } catch (error) { 124 const errorMessage = error instanceof Error 125 ? error.message 126 : String(error); 127 setRetryCount((prev) => prev + 1); 128 if (retryCount >= 1) { 129 setShowContinueAnyway(true); 130 } 131 132 setError(errorMessage); 133 setStatus("error"); 134 onStepError(errorMessage, true); 135 } 136 }; 137 138 const retryVerification = async () => { 139 await verifyDataMigration(); 140 }; 141 142 const continueAnyway = () => { 143 setStatus("completed"); 144 setShowContinueAnyway(false); 145 onStepComplete(); 146 }; 147 148 return ( 149 <MigrationStep 150 name="Migrate Data" 151 status={status} 152 error={error} 153 isVerificationError={status === "error" && 154 error?.includes("Verification failed")} 155 index={1} 156 onRetryVerification={retryVerification} 157 > 158 {status === "error" && showContinueAnyway && ( 159 <div class="flex space-x-2 mt-2"> 160 <button 161 type="button" 162 onClick={continueAnyway} 163 class="px-3 py-1 text-xs bg-white border border-gray-300 text-gray-700 hover:bg-gray-100 rounded transition-colors duration-200 164 dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700" 165 > 166 Continue Anyway 167 </button> 168 </div> 169 )} 170 </MigrationStep> 171 ); 172}