+81
-1
frontend/src/components/migration/InboundWizard.svelte
+81
-1
frontend/src/components/migration/InboundWizard.svelte
···
22
let checkingHandle = $state(false)
23
24
const isResumedMigration = $derived(flow.state.progress.repoImported)
25
26
$effect(() => {
27
if (flow.state.step === 'welcome' || flow.state.step === 'choose-handle') {
···
187
}
188
}
189
190
-
const steps = ['Login', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Verify PLC', 'Complete']
191
function getCurrentStepIndex(): number {
192
switch (flow.state.step) {
193
case 'welcome':
···
197
case 'migrating': return 3
198
case 'email-verify': return 4
199
case 'plc-token':
200
case 'finalizing': return 5
201
case 'success': return 6
202
default: return 0
···
589
</form>
590
</div>
591
592
{:else if flow.state.step === 'finalizing'}
593
<div class="step-content">
594
<h2>Finalizing Migration</h2>
···
1020
padding: var(--space-4);
1021
border-radius: var(--radius-lg);
1022
margin-bottom: var(--space-5);
1023
}
1024
</style>
···
22
let checkingHandle = $state(false)
23
24
const isResumedMigration = $derived(flow.state.progress.repoImported)
25
+
const isDidWeb = $derived(flow.state.sourceDid.startsWith("did:web:"))
26
27
$effect(() => {
28
if (flow.state.step === 'welcome' || flow.state.step === 'choose-handle') {
···
188
}
189
}
190
191
+
async function completeDidWeb() {
192
+
loading = true
193
+
try {
194
+
await flow.completeDidWebMigration()
195
+
} catch (err) {
196
+
flow.setError((err as Error).message)
197
+
} finally {
198
+
loading = false
199
+
}
200
+
}
201
+
202
+
const steps = $derived(isDidWeb
203
+
? ['Login', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Update DID', 'Complete']
204
+
: ['Login', 'Handle', 'Review', 'Transfer', 'Verify Email', 'Verify PLC', 'Complete'])
205
function getCurrentStepIndex(): number {
206
switch (flow.state.step) {
207
case 'welcome':
···
211
case 'migrating': return 3
212
case 'email-verify': return 4
213
case 'plc-token':
214
+
case 'did-web-update':
215
case 'finalizing': return 5
216
case 'success': return 6
217
default: return 0
···
604
</form>
605
</div>
606
607
+
{:else if flow.state.step === 'did-web-update'}
608
+
<div class="step-content">
609
+
<h2>{$_('migration.inbound.didWebUpdate.title')}</h2>
610
+
<p>{$_('migration.inbound.didWebUpdate.desc')}</p>
611
+
612
+
<div class="info-box">
613
+
<p>
614
+
{$_('migration.inbound.didWebUpdate.yourDid')} <code>{flow.state.sourceDid}</code>
615
+
</p>
616
+
<p style="margin-top: 12px;">
617
+
{$_('migration.inbound.didWebUpdate.updateInstructions')}
618
+
</p>
619
+
</div>
620
+
621
+
<div class="code-block">
622
+
<pre>{`{
623
+
"id": "${flow.state.sourceDid}",
624
+
"service": [
625
+
{
626
+
"id": "#atproto_pds",
627
+
"type": "AtprotoPersonalDataServer",
628
+
"serviceEndpoint": "${window.location.origin}"
629
+
}
630
+
]
631
+
}`}</pre>
632
+
</div>
633
+
634
+
<div class="warning-box">
635
+
<strong>{$_('migration.inbound.didWebUpdate.important')}</strong> {$_('migration.inbound.didWebUpdate.verifyFirst')}
636
+
{$_('migration.inbound.didWebUpdate.fileLocation')} <code>https://{flow.state.sourceDid.replace('did:web:', '')}/.well-known/did.json</code>
637
+
</div>
638
+
639
+
<div class="button-row">
640
+
<button class="ghost" onclick={() => flow.setStep('email-verify')} disabled={loading}>Back</button>
641
+
<button onclick={completeDidWeb} disabled={loading}>
642
+
{loading ? $_('migration.inbound.didWebUpdate.completing') : $_('migration.inbound.didWebUpdate.complete')}
643
+
</button>
644
+
</div>
645
+
</div>
646
+
647
{:else if flow.state.step === 'finalizing'}
648
<div class="step-content">
649
<h2>Finalizing Migration</h2>
···
1075
padding: var(--space-4);
1076
border-radius: var(--radius-lg);
1077
margin-bottom: var(--space-5);
1078
+
}
1079
+
1080
+
.code-block {
1081
+
background: var(--bg-primary);
1082
+
border: 1px solid var(--border);
1083
+
border-radius: var(--radius-lg);
1084
+
padding: var(--space-4);
1085
+
margin-bottom: var(--space-5);
1086
+
overflow-x: auto;
1087
+
}
1088
+
1089
+
.code-block pre {
1090
+
margin: 0;
1091
+
font-family: var(--font-mono);
1092
+
font-size: var(--text-sm);
1093
+
white-space: pre-wrap;
1094
+
word-break: break-all;
1095
+
}
1096
+
1097
+
code {
1098
+
font-family: var(--font-mono);
1099
+
background: var(--bg-primary);
1100
+
padding: 2px 6px;
1101
+
border-radius: var(--radius-sm);
1102
+
font-size: 0.9em;
1103
}
1104
</style>
+63
-5
frontend/src/lib/migration/flow.svelte.ts
+63
-5
frontend/src/lib/migration/flow.svelte.ts
···
371
return;
372
}
373
374
-
setProgress({ currentOperation: "Requesting PLC operation token..." });
375
-
await sourceClient.requestPlcOperationSignature();
376
-
setStep("plc-token");
377
} catch (e) {
378
const err = e as Error & { error?: string; status?: number };
379
const message = err.message || err.error ||
···
401
state.targetEmail,
402
state.targetPassword,
403
);
404
-
await sourceClient.requestPlcOperationSignature();
405
-
setStep("plc-token");
406
return true;
407
} catch (e) {
408
const err = e as Error & { error?: string };
···
543
await sourceClient.requestPlcOperationSignature();
544
}
545
546
function reset(): void {
547
state = {
548
direction: "inbound",
···
614
requestPlcToken,
615
submitPlcToken,
616
resendPlcToken,
617
reset,
618
resumeFromState,
619
getLocalSession,
···
371
return;
372
}
373
374
+
if (state.sourceDid.startsWith("did:web:")) {
375
+
setStep("did-web-update");
376
+
} else {
377
+
setProgress({ currentOperation: "Requesting PLC operation token..." });
378
+
await sourceClient.requestPlcOperationSignature();
379
+
setStep("plc-token");
380
+
}
381
} catch (e) {
382
const err = e as Error & { error?: string; status?: number };
383
const message = err.message || err.error ||
···
405
state.targetEmail,
406
state.targetPassword,
407
);
408
+
if (state.sourceDid.startsWith("did:web:")) {
409
+
setStep("did-web-update");
410
+
} else {
411
+
await sourceClient.requestPlcOperationSignature();
412
+
setStep("plc-token");
413
+
}
414
return true;
415
} catch (e) {
416
const err = e as Error & { error?: string };
···
551
await sourceClient.requestPlcOperationSignature();
552
}
553
554
+
async function completeDidWebMigration(): Promise<void> {
555
+
migrationLog("completeDidWebMigration START", {
556
+
sourceDid: state.sourceDid,
557
+
sourceHandle: state.sourceHandle,
558
+
targetHandle: state.targetHandle,
559
+
});
560
+
561
+
if (!sourceClient || !localClient) {
562
+
migrationLog("completeDidWebMigration ERROR: Not connected to PDSes");
563
+
throw new Error("Not connected to PDSes");
564
+
}
565
+
566
+
setStep("finalizing");
567
+
setProgress({ currentOperation: "Activating account..." });
568
+
569
+
try {
570
+
migrationLog("Activating account on NEW PDS");
571
+
const activateStart = Date.now();
572
+
await localClient.activateAccount();
573
+
migrationLog("Account activated", { durationMs: Date.now() - activateStart });
574
+
setProgress({ activated: true });
575
+
576
+
setProgress({ currentOperation: "Deactivating old account..." });
577
+
migrationLog("Deactivating account on OLD PDS");
578
+
const deactivateStart = Date.now();
579
+
try {
580
+
await sourceClient.deactivateAccount();
581
+
migrationLog("Account deactivated on OLD PDS", {
582
+
durationMs: Date.now() - deactivateStart,
583
+
});
584
+
setProgress({ deactivated: true });
585
+
} catch (deactivateErr) {
586
+
const err = deactivateErr as Error & { error?: string };
587
+
migrationLog("Could not deactivate on OLD PDS", { error: err.message });
588
+
}
589
+
590
+
migrationLog("completeDidWebMigration SUCCESS");
591
+
setStep("success");
592
+
clearMigrationState();
593
+
} catch (e) {
594
+
const err = e as Error & { error?: string; status?: number };
595
+
const message = err.message || err.error ||
596
+
`Unknown error (status ${err.status || "unknown"})`;
597
+
migrationLog("completeDidWebMigration FAILED", { error: message });
598
+
setError(message);
599
+
setStep("did-web-update");
600
+
}
601
+
}
602
+
603
function reset(): void {
604
state = {
605
direction: "inbound",
···
671
requestPlcToken,
672
submitPlcToken,
673
resendPlcToken,
674
+
completeDidWebMigration,
675
reset,
676
resumeFromState,
677
getLocalSession,
+1
frontend/src/lib/migration/types.ts
+1
frontend/src/lib/migration/types.ts
+11
frontend/src/locales/en.json
+11
frontend/src/locales/en.json
···
1106
"resend": "Resend Token",
1107
"resending": "Resending..."
1108
},
1109
+
"didWebUpdate": {
1110
+
"title": "Update Your DID Document",
1111
+
"desc": "Since you're using a did:web identity, you need to update your DID document to point to this PDS.",
1112
+
"yourDid": "Your DID is:",
1113
+
"updateInstructions": "Update the did.json file at your domain to point the atproto_pds service endpoint to this PDS:",
1114
+
"important": "Important:",
1115
+
"verifyFirst": "Make sure your DID document is updated and publicly accessible before completing the migration.",
1116
+
"fileLocation": "The file should be at:",
1117
+
"complete": "Complete Migration",
1118
+
"completing": "Completing..."
1119
+
},
1120
"finalizing": {
1121
"title": "Finalizing Migration",
1122
"desc": "Please wait while we complete the migration...",
+11
frontend/src/locales/fi.json
+11
frontend/src/locales/fi.json
···
1106
"resend": "Lähetä uudelleen",
1107
"resending": "Lähetetään..."
1108
},
1109
+
"didWebUpdate": {
1110
+
"title": "Päivitä DID-dokumenttisi",
1111
+
"desc": "Koska käytät did:web-identiteettiä, sinun täytyy päivittää DID-dokumenttisi osoittamaan tähän PDS:ään.",
1112
+
"yourDid": "DID:si on:",
1113
+
"updateInstructions": "Päivitä verkkotunnuksesi did.json-tiedosto niin, että atproto_pds-palvelun päätepiste osoittaa tähän PDS:ään:",
1114
+
"important": "Tärkeää:",
1115
+
"verifyFirst": "Varmista, että DID-dokumenttisi on päivitetty ja julkisesti saatavilla ennen siirron viimeistelyä.",
1116
+
"fileLocation": "Tiedoston tulee sijaita:",
1117
+
"complete": "Viimeistele siirto",
1118
+
"completing": "Viimeistellään..."
1119
+
},
1120
"finalizing": {
1121
"title": "Viimeistellään siirtoa",
1122
"desc": "Odota, kun viimeistelemme siirtoa...",
+11
frontend/src/locales/ja.json
+11
frontend/src/locales/ja.json
···
1106
"resend": "再送信",
1107
"resending": "送信中..."
1108
},
1109
+
"didWebUpdate": {
1110
+
"title": "DIDドキュメントを更新",
1111
+
"desc": "did:webアイデンティティを使用しているため、DIDドキュメントを更新してこのPDSを指すようにする必要があります。",
1112
+
"yourDid": "あなたのDID:",
1113
+
"updateInstructions": "ドメインのdid.jsonファイルを更新して、atproto_pdsサービスエンドポイントをこのPDSに向けてください:",
1114
+
"important": "重要:",
1115
+
"verifyFirst": "移行を完了する前に、DIDドキュメントが更新され、公開アクセス可能であることを確認してください。",
1116
+
"fileLocation": "ファイルの場所:",
1117
+
"complete": "移行を完了",
1118
+
"completing": "完了中..."
1119
+
},
1120
"finalizing": {
1121
"title": "移行を完了中",
1122
"desc": "移行を完了しています...",
+11
frontend/src/locales/ko.json
+11
frontend/src/locales/ko.json
···
1106
"resend": "재전송",
1107
"resending": "전송 중..."
1108
},
1109
+
"didWebUpdate": {
1110
+
"title": "DID 문서 업데이트",
1111
+
"desc": "did:web 아이덴티티를 사용하고 있으므로 DID 문서를 이 PDS를 가리키도록 업데이트해야 합니다.",
1112
+
"yourDid": "당신의 DID:",
1113
+
"updateInstructions": "도메인의 did.json 파일을 업데이트하여 atproto_pds 서비스 엔드포인트가 이 PDS를 가리키도록 하세요:",
1114
+
"important": "중요:",
1115
+
"verifyFirst": "마이그레이션을 완료하기 전에 DID 문서가 업데이트되고 공개적으로 접근 가능한지 확인하세요.",
1116
+
"fileLocation": "파일 위치:",
1117
+
"complete": "마이그레이션 완료",
1118
+
"completing": "완료 중..."
1119
+
},
1120
"finalizing": {
1121
"title": "마이그레이션 완료 중",
1122
"desc": "마이그레이션을 완료하는 중입니다...",
+11
frontend/src/locales/sv.json
+11
frontend/src/locales/sv.json
···
1106
"resend": "Skicka igen",
1107
"resending": "Skickar..."
1108
},
1109
+
"didWebUpdate": {
1110
+
"title": "Uppdatera ditt DID-dokument",
1111
+
"desc": "Eftersom du använder en did:web-identitet måste du uppdatera ditt DID-dokument för att peka på denna PDS.",
1112
+
"yourDid": "Ditt DID är:",
1113
+
"updateInstructions": "Uppdatera did.json-filen på din domän så att atproto_pds-tjänstens slutpunkt pekar på denna PDS:",
1114
+
"important": "Viktigt:",
1115
+
"verifyFirst": "Se till att ditt DID-dokument är uppdaterat och offentligt tillgängligt innan du slutför flytten.",
1116
+
"fileLocation": "Filen ska finnas på:",
1117
+
"complete": "Slutför flytt",
1118
+
"completing": "Slutför..."
1119
+
},
1120
"finalizing": {
1121
"title": "Slutför flytt",
1122
"desc": "Vänta medan vi slutför flytten...",
+11
frontend/src/locales/zh.json
+11
frontend/src/locales/zh.json
···
1106
"resend": "重新发送",
1107
"resending": "发送中..."
1108
},
1109
+
"didWebUpdate": {
1110
+
"title": "更新您的DID文档",
1111
+
"desc": "由于您使用的是did:web身份,您需要更新DID文档以指向此PDS。",
1112
+
"yourDid": "您的DID是:",
1113
+
"updateInstructions": "更新您域名上的did.json文件,将atproto_pds服务端点指向此PDS:",
1114
+
"important": "重要提示:",
1115
+
"verifyFirst": "在完成迁移之前,请确保您的DID文档已更新并可公开访问。",
1116
+
"fileLocation": "文件应位于:",
1117
+
"complete": "完成迁移",
1118
+
"completing": "完成中..."
1119
+
},
1120
"finalizing": {
1121
"title": "正在完成迁移",
1122
"desc": "请稍候,正在完成迁移...",
+58
-5
frontend/src/routes/RepoExplorer.svelte
+58
-5
frontend/src/routes/RepoExplorer.svelte
···
495
.back {
496
color: var(--text-secondary);
497
text-decoration: none;
498
}
499
500
.back:hover {
501
color: var(--accent);
502
}
503
504
.sep {
···
508
.breadcrumb-link {
509
background: none;
510
border: none;
511
-
padding: 0;
512
color: var(--accent);
513
cursor: pointer;
514
font-size: inherit;
515
}
516
517
.breadcrumb-link:hover {
518
text-decoration: underline;
519
}
520
521
.current {
···
683
align-items: center;
684
width: 100%;
685
padding: var(--space-3);
686
-
background: var(--bg-card);
687
border: 1px solid var(--border-color);
688
border-radius: var(--radius-md);
689
cursor: pointer;
690
text-align: left;
691
color: var(--text-primary);
692
-
transition: border-color var(--transition-fast);
693
}
694
695
.collection-link:hover {
696
border-color: var(--accent);
697
}
698
699
.nsid {
700
font-weight: var(--font-medium);
701
color: var(--accent);
···
705
color: var(--text-muted);
706
}
707
708
.record-list {
709
list-style: none;
710
padding: 0;
···
718
display: block;
719
width: 100%;
720
padding: var(--space-4);
721
-
background: var(--bg-card);
722
border: 1px solid var(--border-color);
723
border-radius: var(--radius-md);
724
cursor: pointer;
725
text-align: left;
726
color: var(--text-primary);
727
-
transition: border-color var(--transition-fast);
728
}
729
730
.record-item:hover {
731
border-color: var(--accent);
732
}
733
734
.record-info {
···
926
background: var(--bg-secondary);
927
padding: var(--space-6);
928
border-radius: var(--radius-xl);
929
}
930
</style>
···
495
.back {
496
color: var(--text-secondary);
497
text-decoration: none;
498
+
padding: var(--space-1) var(--space-2);
499
+
margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-2));
500
+
border-radius: var(--radius-sm);
501
+
transition: background var(--transition-fast), color var(--transition-fast);
502
}
503
504
.back:hover {
505
color: var(--accent);
506
+
background: var(--accent-muted);
507
+
}
508
+
509
+
.back:focus {
510
+
outline: 2px solid var(--accent);
511
+
outline-offset: 2px;
512
}
513
514
.sep {
···
518
.breadcrumb-link {
519
background: none;
520
border: none;
521
+
padding: var(--space-1) var(--space-2);
522
+
margin: calc(-1 * var(--space-1)) calc(-1 * var(--space-2));
523
color: var(--accent);
524
cursor: pointer;
525
font-size: inherit;
526
+
border-radius: var(--radius-sm);
527
+
transition: background var(--transition-fast);
528
}
529
530
.breadcrumb-link:hover {
531
+
background: var(--accent-muted);
532
text-decoration: underline;
533
+
}
534
+
535
+
.breadcrumb-link:focus {
536
+
outline: 2px solid var(--accent);
537
+
outline-offset: 2px;
538
}
539
540
.current {
···
702
align-items: center;
703
width: 100%;
704
padding: var(--space-3);
705
+
background: var(--bg-primary);
706
border: 1px solid var(--border-color);
707
border-radius: var(--radius-md);
708
cursor: pointer;
709
text-align: left;
710
color: var(--text-primary);
711
+
transition: background var(--transition-fast), border-color var(--transition-fast);
712
}
713
714
.collection-link:hover {
715
+
background: var(--bg-secondary);
716
border-color: var(--accent);
717
}
718
719
+
.collection-link:focus {
720
+
outline: 2px solid var(--accent);
721
+
outline-offset: 2px;
722
+
}
723
+
724
+
.collection-link:active {
725
+
background: var(--bg-tertiary);
726
+
}
727
+
728
.nsid {
729
font-weight: var(--font-medium);
730
color: var(--accent);
···
734
color: var(--text-muted);
735
}
736
737
+
.collection-link:hover .arrow {
738
+
color: var(--accent);
739
+
}
740
+
741
.record-list {
742
list-style: none;
743
padding: 0;
···
751
display: block;
752
width: 100%;
753
padding: var(--space-4);
754
+
background: var(--bg-primary);
755
border: 1px solid var(--border-color);
756
border-radius: var(--radius-md);
757
cursor: pointer;
758
text-align: left;
759
color: var(--text-primary);
760
+
transition: background var(--transition-fast), border-color var(--transition-fast);
761
}
762
763
.record-item:hover {
764
+
background: var(--bg-secondary);
765
border-color: var(--accent);
766
+
}
767
+
768
+
.record-item:focus {
769
+
outline: 2px solid var(--accent);
770
+
outline-offset: 2px;
771
+
}
772
+
773
+
.record-item:active {
774
+
background: var(--bg-tertiary);
775
}
776
777
.record-info {
···
969
background: var(--bg-secondary);
970
padding: var(--space-6);
971
border-radius: var(--radius-xl);
972
+
}
973
+
974
+
.page ::selection {
975
+
background: var(--accent);
976
+
color: var(--text-inverse);
977
+
}
978
+
979
+
.page ::-moz-selection {
980
+
background: var(--accent);
981
+
color: var(--text-inverse);
982
}
983
</style>