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}