Graphical PDS migrator for AT Protocol

Add verification retries (#7)

* retry verification

* Update MigrationProgress.tsx

authored by Roscoe Rubin-Rottenberg and committed by GitHub fc5ad00e 650f8106

Changed files
+340 -213
islands
+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 ===