+340
-213
islands/MigrationProgress.tsx
+340
-213
islands/MigrationProgress.tsx
···
30
30
name: string;
31
31
status: "pending" | "in-progress" | "verifying" | "completed" | "error";
32
32
error?: string;
33
+
isVerificationError?: boolean;
33
34
}
34
35
35
36
/**
···
43
44
const [migrationState, setMigrationState] = useState<
44
45
MigrationStateInfo | null
45
46
>(null);
47
+
const [retryAttempts, setRetryAttempts] = useState<Record<number, number>>(
48
+
{},
49
+
);
50
+
const [showContinueAnyway, setShowContinueAnyway] = useState<
51
+
Record<number, boolean>
52
+
>({});
46
53
47
54
const [steps, setSteps] = useState<MigrationStep[]>([
48
55
{ name: "Create Account", status: "pending" },
···
55
62
index: number,
56
63
status: MigrationStep["status"],
57
64
error?: string,
65
+
isVerificationError?: boolean,
58
66
) => {
59
67
console.log(
60
68
`Updating step ${index} to ${status}${
···
64
72
setSteps((prevSteps) =>
65
73
prevSteps.map((step, i) =>
66
74
i === index
67
-
? { ...step, status, error }
75
+
? { ...step, status, error, isVerificationError }
68
76
: i > index
69
-
? { ...step, status: "pending", error: undefined }
77
+
? {
78
+
...step,
79
+
status: "pending",
80
+
error: undefined,
81
+
isVerificationError: undefined,
82
+
}
70
83
: step
71
84
)
72
85
);
···
228
241
updateStepStatus(0, "verifying");
229
242
const verified = await verifyStep(0);
230
243
if (!verified) {
231
-
throw new Error("Account creation verification failed");
232
-
}
233
-
} catch (error) {
234
-
updateStepStatus(
235
-
0,
236
-
"error",
237
-
error instanceof Error ? error.message : String(error),
238
-
);
239
-
throw error;
240
-
}
241
-
242
-
// Step 2: Migrate Data
243
-
updateStepStatus(1, "in-progress");
244
-
console.log("Starting data migration...");
245
-
246
-
try {
247
-
// Step 2.1: Migrate Repo
248
-
console.log("Data migration: Starting repo migration");
249
-
const repoRes = await fetch("/api/migrate/data/repo", {
250
-
method: "POST",
251
-
headers: { "Content-Type": "application/json" },
252
-
});
253
-
254
-
console.log("Repo migration: Response status:", repoRes.status);
255
-
const repoText = await repoRes.text();
256
-
console.log("Repo migration: Raw response:", repoText);
257
-
258
-
if (!repoRes.ok) {
259
-
try {
260
-
const json = JSON.parse(repoText);
261
-
console.error("Repo migration: Error response:", json);
262
-
throw new Error(json.message || "Failed to migrate repo");
263
-
} catch {
264
-
console.error("Repo migration: Non-JSON error response:", repoText);
265
-
throw new Error(repoText || "Failed to migrate repo");
266
-
}
267
-
}
268
-
269
-
// Step 2.2: Migrate Blobs
270
-
console.log("Data migration: Starting blob migration");
271
-
const blobsRes = await fetch("/api/migrate/data/blobs", {
272
-
method: "POST",
273
-
headers: { "Content-Type": "application/json" },
274
-
});
275
-
276
-
console.log("Blob migration: Response status:", blobsRes.status);
277
-
const blobsText = await blobsRes.text();
278
-
console.log("Blob migration: Raw response:", blobsText);
279
-
280
-
if (!blobsRes.ok) {
281
-
try {
282
-
const json = JSON.parse(blobsText);
283
-
console.error("Blob migration: Error response:", json);
284
-
throw new Error(json.message || "Failed to migrate blobs");
285
-
} catch {
286
-
console.error(
287
-
"Blob migration: Non-JSON error response:",
288
-
blobsText,
289
-
);
290
-
throw new Error(blobsText || "Failed to migrate blobs");
291
-
}
292
-
}
293
-
294
-
// Step 2.3: Migrate Preferences
295
-
console.log("Data migration: Starting preferences migration");
296
-
const prefsRes = await fetch("/api/migrate/data/prefs", {
297
-
method: "POST",
298
-
headers: { "Content-Type": "application/json" },
299
-
});
300
-
301
-
console.log("Preferences migration: Response status:", prefsRes.status);
302
-
const prefsText = await prefsRes.text();
303
-
console.log("Preferences migration: Raw response:", prefsText);
304
-
305
-
if (!prefsRes.ok) {
306
-
try {
307
-
const json = JSON.parse(prefsText);
308
-
console.error("Preferences migration: Error response:", json);
309
-
throw new Error(json.message || "Failed to migrate preferences");
310
-
} catch {
311
-
console.error(
312
-
"Preferences migration: Non-JSON error response:",
313
-
prefsText,
314
-
);
315
-
throw new Error(prefsText || "Failed to migrate preferences");
316
-
}
317
-
}
318
-
319
-
console.log("Data migration: Starting verification");
320
-
updateStepStatus(1, "verifying");
321
-
const verified = await verifyStep(1);
322
-
console.log("Data migration: Verification result:", verified);
323
-
if (!verified) {
324
-
throw new Error("Data migration verification failed");
325
-
}
326
-
} catch (error) {
327
-
console.error("Data migration: Error caught:", error);
328
-
updateStepStatus(
329
-
1,
330
-
"error",
331
-
error instanceof Error ? error.message : String(error),
332
-
);
333
-
throw error;
334
-
}
335
-
336
-
// Step 3: Request Identity Migration
337
-
updateStepStatus(2, "in-progress");
338
-
console.log("Requesting identity migration...");
339
-
340
-
try {
341
-
const requestRes = await fetch("/api/migrate/identity/request", {
342
-
method: "POST",
343
-
headers: { "Content-Type": "application/json" },
344
-
});
345
-
346
-
console.log("Identity request response status:", requestRes.status);
347
-
const requestText = await requestRes.text();
348
-
console.log("Identity request response:", requestText);
349
-
350
-
if (!requestRes.ok) {
351
-
try {
352
-
const json = JSON.parse(requestText);
353
-
throw new Error(
354
-
json.message || "Failed to request identity migration",
355
-
);
356
-
} catch {
357
-
throw new Error(
358
-
requestText || "Failed to request identity migration",
359
-
);
360
-
}
361
-
}
362
-
363
-
try {
364
-
const jsonData = JSON.parse(requestText);
365
-
if (!jsonData.success) {
366
-
throw new Error(
367
-
jsonData.message || "Identity migration request failed",
368
-
);
369
-
}
370
-
console.log("Identity migration requested successfully");
371
-
372
-
// Update step name to prompt for token
373
-
setSteps((prevSteps) =>
374
-
prevSteps.map((step, i) =>
375
-
i === 2
376
-
? {
377
-
...step,
378
-
name:
379
-
"Enter the token sent to your email to complete identity migration",
380
-
}
381
-
: step
382
-
)
244
+
console.log(
245
+
"Account creation: Verification failed, waiting for user action",
383
246
);
384
-
// Don't continue with migration - wait for token input
385
247
return;
386
-
} catch (e) {
387
-
console.error("Failed to parse identity request response:", e);
388
-
throw new Error(
389
-
"Invalid response from server during identity request",
390
-
);
391
248
}
249
+
250
+
// If verification succeeds, continue to data migration
251
+
await startDataMigration();
392
252
} catch (error) {
393
253
updateStepStatus(
394
-
2,
254
+
0,
395
255
"error",
396
256
error instanceof Error ? error.message : String(error),
397
257
);
···
441
301
updateStepStatus(2, "verifying");
442
302
const verified = await verifyStep(2);
443
303
if (!verified) {
444
-
throw new Error("Identity migration verification failed");
445
-
}
446
-
447
-
// Step 4: Finalize Migration
448
-
updateStepStatus(3, "in-progress");
449
-
try {
450
-
const finalizeRes = await fetch("/api/migrate/finalize", {
451
-
method: "POST",
452
-
headers: { "Content-Type": "application/json" },
453
-
});
454
-
455
-
const finalizeData = await finalizeRes.text();
456
-
if (!finalizeRes.ok) {
457
-
try {
458
-
const json = JSON.parse(finalizeData);
459
-
throw new Error(json.message || "Failed to finalize migration");
460
-
} catch {
461
-
throw new Error(finalizeData || "Failed to finalize migration");
462
-
}
463
-
}
464
-
465
-
try {
466
-
const jsonData = JSON.parse(finalizeData);
467
-
if (!jsonData.success) {
468
-
throw new Error(jsonData.message || "Finalization failed");
469
-
}
470
-
} catch {
471
-
throw new Error("Invalid response from server during finalization");
472
-
}
473
-
474
-
updateStepStatus(3, "verifying");
475
-
const verified = await verifyStep(3);
476
-
if (!verified) {
477
-
throw new Error("Migration finalization verification failed");
478
-
}
479
-
} catch (error) {
480
-
updateStepStatus(
481
-
3,
482
-
"error",
483
-
error instanceof Error ? error.message : String(error),
304
+
console.log(
305
+
"Identity migration: Verification failed, waiting for user action",
484
306
);
485
-
throw error;
307
+
return;
486
308
}
309
+
310
+
// If verification succeeds, continue to finalization
311
+
await startFinalization();
487
312
} catch (error) {
488
313
console.error("Identity migration error:", error);
489
314
updateStepStatus(
···
584
409
if (data.ready) {
585
410
console.log(`Verification: Step ${stepNum + 1} is ready`);
586
411
updateStepStatus(stepNum, "completed");
412
+
// Reset retry state on success
413
+
setRetryAttempts((prev) => ({ ...prev, [stepNum]: 0 }));
414
+
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: false }));
415
+
416
+
// Continue to next step if not the last one
417
+
if (stepNum < 3) {
418
+
setTimeout(() => continueToNextStep(stepNum + 1), 500);
419
+
}
420
+
587
421
return true;
588
422
} else {
589
423
console.log(
···
609
443
const errorMessage = `${
610
444
data.reason || "Verification failed"
611
445
}\nStatus details: ${JSON.stringify(statusDetails, null, 2)}`;
612
-
updateStepStatus(stepNum, "error", errorMessage);
446
+
447
+
// Track retry attempts
448
+
const currentAttempts = retryAttempts[stepNum] || 0;
449
+
setRetryAttempts((prev) => ({
450
+
...prev,
451
+
[stepNum]: currentAttempts + 1,
452
+
}));
453
+
454
+
// Show continue anyway option if this is the second failure
455
+
if (currentAttempts >= 1) {
456
+
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: true }));
457
+
}
458
+
459
+
updateStepStatus(stepNum, "error", errorMessage, true);
613
460
return false;
614
461
}
615
462
} catch (e) {
616
463
console.error(`Verification: Error in step ${stepNum + 1}:`, e);
464
+
const currentAttempts = retryAttempts[stepNum] || 0;
465
+
setRetryAttempts((prev) => ({ ...prev, [stepNum]: currentAttempts + 1 }));
466
+
467
+
// Show continue anyway option if this is the second failure
468
+
if (currentAttempts >= 1) {
469
+
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: true }));
470
+
}
471
+
617
472
updateStepStatus(
618
473
stepNum,
619
474
"error",
620
475
e instanceof Error ? e.message : String(e),
476
+
true,
621
477
);
622
478
return false;
623
479
}
624
480
};
625
481
482
+
const retryVerification = async (stepNum: number) => {
483
+
console.log(`Retrying verification for step ${stepNum + 1}`);
484
+
await verifyStep(stepNum);
485
+
};
486
+
487
+
const continueAnyway = (stepNum: number) => {
488
+
console.log(`Continuing anyway for step ${stepNum + 1}`);
489
+
updateStepStatus(stepNum, "completed");
490
+
setShowContinueAnyway((prev) => ({ ...prev, [stepNum]: false }));
491
+
492
+
// Continue with next step if not the last one
493
+
if (stepNum < 3) {
494
+
continueToNextStep(stepNum + 1);
495
+
}
496
+
};
497
+
498
+
const continueToNextStep = async (stepNum: number) => {
499
+
switch (stepNum) {
500
+
case 1:
501
+
// Continue to data migration
502
+
await startDataMigration();
503
+
break;
504
+
case 2:
505
+
// Continue to identity migration
506
+
await startIdentityMigration();
507
+
break;
508
+
case 3:
509
+
// Continue to finalization
510
+
await startFinalization();
511
+
break;
512
+
}
513
+
};
514
+
515
+
const startDataMigration = async () => {
516
+
// Step 2: Migrate Data
517
+
updateStepStatus(1, "in-progress");
518
+
console.log("Starting data migration...");
519
+
520
+
try {
521
+
// Step 2.1: Migrate Repo
522
+
console.log("Data migration: Starting repo migration");
523
+
const repoRes = await fetch("/api/migrate/data/repo", {
524
+
method: "POST",
525
+
headers: { "Content-Type": "application/json" },
526
+
});
527
+
528
+
console.log("Repo migration: Response status:", repoRes.status);
529
+
const repoText = await repoRes.text();
530
+
console.log("Repo migration: Raw response:", repoText);
531
+
532
+
if (!repoRes.ok) {
533
+
try {
534
+
const json = JSON.parse(repoText);
535
+
console.error("Repo migration: Error response:", json);
536
+
throw new Error(json.message || "Failed to migrate repo");
537
+
} catch {
538
+
console.error("Repo migration: Non-JSON error response:", repoText);
539
+
throw new Error(repoText || "Failed to migrate repo");
540
+
}
541
+
}
542
+
543
+
// Step 2.2: Migrate Blobs
544
+
console.log("Data migration: Starting blob migration");
545
+
const blobsRes = await fetch("/api/migrate/data/blobs", {
546
+
method: "POST",
547
+
headers: { "Content-Type": "application/json" },
548
+
});
549
+
550
+
console.log("Blob migration: Response status:", blobsRes.status);
551
+
const blobsText = await blobsRes.text();
552
+
console.log("Blob migration: Raw response:", blobsText);
553
+
554
+
if (!blobsRes.ok) {
555
+
try {
556
+
const json = JSON.parse(blobsText);
557
+
console.error("Blob migration: Error response:", json);
558
+
throw new Error(json.message || "Failed to migrate blobs");
559
+
} catch {
560
+
console.error(
561
+
"Blob migration: Non-JSON error response:",
562
+
blobsText,
563
+
);
564
+
throw new Error(blobsText || "Failed to migrate blobs");
565
+
}
566
+
}
567
+
568
+
// Step 2.3: Migrate Preferences
569
+
console.log("Data migration: Starting preferences migration");
570
+
const prefsRes = await fetch("/api/migrate/data/prefs", {
571
+
method: "POST",
572
+
headers: { "Content-Type": "application/json" },
573
+
});
574
+
575
+
console.log("Preferences migration: Response status:", prefsRes.status);
576
+
const prefsText = await prefsRes.text();
577
+
console.log("Preferences migration: Raw response:", prefsText);
578
+
579
+
if (!prefsRes.ok) {
580
+
try {
581
+
const json = JSON.parse(prefsText);
582
+
console.error("Preferences migration: Error response:", json);
583
+
throw new Error(json.message || "Failed to migrate preferences");
584
+
} catch {
585
+
console.error(
586
+
"Preferences migration: Non-JSON error response:",
587
+
prefsText,
588
+
);
589
+
throw new Error(prefsText || "Failed to migrate preferences");
590
+
}
591
+
}
592
+
593
+
console.log("Data migration: Starting verification");
594
+
updateStepStatus(1, "verifying");
595
+
const verified = await verifyStep(1);
596
+
console.log("Data migration: Verification result:", verified);
597
+
if (!verified) {
598
+
console.log(
599
+
"Data migration: Verification failed, waiting for user action",
600
+
);
601
+
return;
602
+
}
603
+
604
+
// If verification succeeds, continue to next step
605
+
await startIdentityMigration();
606
+
} catch (error) {
607
+
console.error("Data migration: Error caught:", error);
608
+
updateStepStatus(
609
+
1,
610
+
"error",
611
+
error instanceof Error ? error.message : String(error),
612
+
);
613
+
throw error;
614
+
}
615
+
};
616
+
617
+
const startIdentityMigration = async () => {
618
+
// Step 3: Request Identity Migration
619
+
updateStepStatus(2, "in-progress");
620
+
console.log("Requesting identity migration...");
621
+
622
+
try {
623
+
const requestRes = await fetch("/api/migrate/identity/request", {
624
+
method: "POST",
625
+
headers: { "Content-Type": "application/json" },
626
+
});
627
+
628
+
console.log("Identity request response status:", requestRes.status);
629
+
const requestText = await requestRes.text();
630
+
console.log("Identity request response:", requestText);
631
+
632
+
if (!requestRes.ok) {
633
+
try {
634
+
const json = JSON.parse(requestText);
635
+
throw new Error(
636
+
json.message || "Failed to request identity migration",
637
+
);
638
+
} catch {
639
+
throw new Error(
640
+
requestText || "Failed to request identity migration",
641
+
);
642
+
}
643
+
}
644
+
645
+
try {
646
+
const jsonData = JSON.parse(requestText);
647
+
if (!jsonData.success) {
648
+
throw new Error(
649
+
jsonData.message || "Identity migration request failed",
650
+
);
651
+
}
652
+
console.log("Identity migration requested successfully");
653
+
654
+
// Update step name to prompt for token
655
+
setSteps((prevSteps) =>
656
+
prevSteps.map((step, i) =>
657
+
i === 2
658
+
? {
659
+
...step,
660
+
name:
661
+
"Enter the token sent to your email to complete identity migration",
662
+
}
663
+
: step
664
+
)
665
+
);
666
+
// Don't continue with migration - wait for token input
667
+
return;
668
+
} catch (e) {
669
+
console.error("Failed to parse identity request response:", e);
670
+
throw new Error(
671
+
"Invalid response from server during identity request",
672
+
);
673
+
}
674
+
} catch (error) {
675
+
updateStepStatus(
676
+
2,
677
+
"error",
678
+
error instanceof Error ? error.message : String(error),
679
+
);
680
+
throw error;
681
+
}
682
+
};
683
+
684
+
const startFinalization = async () => {
685
+
// Step 4: Finalize Migration
686
+
updateStepStatus(3, "in-progress");
687
+
try {
688
+
const finalizeRes = await fetch("/api/migrate/finalize", {
689
+
method: "POST",
690
+
headers: { "Content-Type": "application/json" },
691
+
});
692
+
693
+
const finalizeData = await finalizeRes.text();
694
+
if (!finalizeRes.ok) {
695
+
try {
696
+
const json = JSON.parse(finalizeData);
697
+
throw new Error(json.message || "Failed to finalize migration");
698
+
} catch {
699
+
throw new Error(finalizeData || "Failed to finalize migration");
700
+
}
701
+
}
702
+
703
+
try {
704
+
const jsonData = JSON.parse(finalizeData);
705
+
if (!jsonData.success) {
706
+
throw new Error(jsonData.message || "Finalization failed");
707
+
}
708
+
} catch {
709
+
throw new Error("Invalid response from server during finalization");
710
+
}
711
+
712
+
updateStepStatus(3, "verifying");
713
+
const verified = await verifyStep(3);
714
+
if (!verified) {
715
+
console.log(
716
+
"Finalization: Verification failed, waiting for user action",
717
+
);
718
+
return;
719
+
}
720
+
} catch (error) {
721
+
updateStepStatus(
722
+
3,
723
+
"error",
724
+
error instanceof Error ? error.message : String(error),
725
+
);
726
+
throw error;
727
+
}
728
+
};
729
+
626
730
return (
627
731
<div class="space-y-8">
628
732
{/* Migration state alert */}
···
675
779
{getStepDisplayName(step, index)}
676
780
</p>
677
781
{step.error && (
678
-
<p class="text-sm text-red-600 dark:text-red-400 mt-1">
679
-
{(() => {
680
-
try {
681
-
const err = JSON.parse(step.error);
682
-
return err.message || step.error;
683
-
} catch {
684
-
return step.error;
685
-
}
686
-
})()}
687
-
</p>
782
+
<div class="mt-1">
783
+
<p class="text-sm text-red-600 dark:text-red-400">
784
+
{(() => {
785
+
try {
786
+
const err = JSON.parse(step.error);
787
+
return err.message || step.error;
788
+
} catch {
789
+
return step.error;
790
+
}
791
+
})()}
792
+
</p>
793
+
{step.isVerificationError && (
794
+
<div class="flex space-x-2 mt-2">
795
+
<button
796
+
type="button"
797
+
onClick={() => retryVerification(index)}
798
+
class="px-3 py-1 text-xs bg-blue-600 hover:bg-blue-700 text-white rounded transition-colors duration-200 dark:bg-blue-500 dark:hover:bg-blue-400"
799
+
>
800
+
Retry Verification
801
+
</button>
802
+
{showContinueAnyway[index] && (
803
+
<button
804
+
type="button"
805
+
onClick={() => continueAnyway(index)}
806
+
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
807
+
dark:bg-gray-800 dark:border-gray-600 dark:text-gray-200 dark:hover:bg-gray-700"
808
+
>
809
+
Continue Anyway
810
+
</button>
811
+
)}
812
+
</div>
813
+
)}
814
+
</div>
688
815
)}
689
816
{index === 2 && step.status === "in-progress" &&
690
817
step.name ===