fix(desktop): clarify migration key setup and small-window layout

This commit is contained in:
二丫讲梵 authored and GitHub committed 2026-09-24 12:35:24 +08:00
1 parent 72f1e5e5eb
commit 82a85e6362
13 files changed
+69 -2

No files matched your search

@@ -119,7 +119,8 @@ async function diagnostic() {
<span class="text-muted-foreground">{{ t("migration.backupRequiredLabel") }}</span> {{ status.backupRequired ? t("migration.yes") : t("migration.no") }}
</div>
<div>
<span class="text-muted-foreground">{{ t("migration.keyProviderLabel") }}</span> {{ status.persistentKeyConfigured ? t("migration.ready") : t("migration.pending") }}
<span class="text-muted-foreground">{{ t("migration.keyProviderLabel") }}</span>
{{ status.keyStatus === "will_create" ? t("migration.keyWillCreate") : status.persistentKeyConfigured ? t("migration.ready") : t("migration.pending") }}
</div>
</div>
<p v-if="status?.dataDir" class="break-all text-xs text-muted-foreground">
@@ -136,6 +137,7 @@ async function diagnostic() {
</button>
</p>
<p v-if="status?.keyProviderAvailable === false && !status.keyCreationAllowed" class="rounded-lg bg-destructive/10 p-3 text-sm text-destructive">{{ t("migration.keyUnavailable") }}</p>
<p v-else-if="status?.keyStatus === 'will_create'" class="rounded-lg bg-primary/5 p-3 text-sm text-muted-foreground">{{ t("migration.keyWillCreateHint") }}</p>
<p v-if="status?.keyFileConfigured === false && status?.persistentKeyConfigured === false && status?.keyCreationAllowed === false" class="text-xs text-muted-foreground">{{ t("migration.keyRecoveryHint") }}</p>
<div v-if="props.store.state.error" class="space-y-1 rounded-lg bg-destructive/10 p-4 text-sm text-destructive">
<p>{{ t(`migration.${props.store.state.error === "statusFailed" ? "statusFailed" : props.store.state.error === "cleanupFailed" ? "cleanupFailed" : "failed"}`) }}</p>
+2
View File
@@ -10978,6 +10978,8 @@ export default {
yes: "yes",
no: "no",
keyUnavailable: "The device key is unavailable. Allow keychain access or configure a persistent key.",
keyWillCreate: "created during first migration",
keyWillCreateHint: "DBX has not created a local encryption key on this device yet. Start the migration to create a persistent key, and allow credential-store access if prompted.",
keyRecoveryHint: "Restore the original key used for this data directory, or configure the same external key that encrypted the existing data. Do not replace it with a new key.",
progress: "Creating backup, migrating, and verifying. Do not close DBX…",
failed: "Migration did not complete. Original data has been retained.",
+2
View File
@@ -10489,6 +10489,8 @@ export default withEnglishFallback({
},
launching: "Iniciando DBX…",
openBackupDir: "Abrir ubicación de copia de seguridad",
keyWillCreate: "Se crea en la primera migración",
keyWillCreateHint: "Este dispositivo aún no tiene una clave de cifrado local de DBX. Después de hacer clic en «Iniciar migración», DBX creará una clave persistente; si el sistema muestra una ventana de autorización de acceso a credenciales, permita el acceso.",
},
solrAdmin: {
title: "Administración de Solr",
+2
View File
@@ -10489,6 +10489,8 @@ export default withEnglishFallback({
},
launching: "Avvio di DBX in corso…",
openBackupDir: "Apri posizione backup",
keyWillCreate: "Creato durante la prima migrazione",
keyWillCreateHint: "Questo dispositivo non dispone ancora di una chiave di crittografia locale DBX. Dopo aver fatto clic su \"Avvia migrazione\", DBX creerà una chiave persistente; se il sistema visualizza una finestra di autorizzazione all'accesso alle credenziali, consenti l'accesso.",
},
solrAdmin: {
title: "Amministrazione Solr",
+2
View File
@@ -10539,6 +10539,8 @@ export default withEnglishFallback({
retry: "再試行",
launching: "DBX を起動しています…",
openBackupDir: "バックアップの場所を開く",
keyWillCreate: "初回移行時に作成",
keyWillCreateHint: "現在のデバイスにはまだ DBX ローカル暗号化キーがありません。「移行を開始」をクリックすると、DBX が永続化キーを作成します。システムが資格情報アクセスの承認ウィンドウを表示した場合は、アクセスを許可してください。",
},
solrAdmin: {
title: "Solr 管理",
+2
View File
@@ -9946,6 +9946,8 @@ export default withEnglishFallback({
},
launching: "DBX를 시작하는 중…",
openBackupDir: "백업 위치 열기",
keyWillCreate: "최초 마이그레이션 시 생성",
keyWillCreateHint: "현재 장치에는 아직 DBX 로컬 암호화 키가 없습니다. “마이그레이션 시작”을 클릭하면 DBX가 영구 키를 생성합니다. 시스템에서 자격 증명 접근 권한 창이 표시되면 접근을 허용해 주세요.",
},
solrAdmin: {
title: "Solr 관리",
+2
View File
@@ -10490,6 +10490,8 @@ export default withEnglishFallback({
},
launching: "Iniciando o DBX…",
openBackupDir: "Abrir local do backup",
keyWillCreate: "Criado na primeira migração",
keyWillCreateHint: "O dispositivo atual ainda não tem uma chave de criptografia local do DBX. Depois de clicar em “Iniciar migração”, o DBX criará uma chave persistente; se o sistema exibir uma janela de autorização de acesso a credenciais, permita o acesso.",
},
solrAdmin: {
title: "Administração do Solr",
+2
View File
@@ -10945,6 +10945,8 @@ export default withEnglishFallback({
yes: "是",
no: "否",
keyUnavailable: "当前设备密钥不可用,请允许钥匙串访问或配置持久化密钥。",
keyWillCreate: "首次迁移时创建",
keyWillCreateHint: "当前设备还没有 DBX 本地加密密钥。点击“开始迁移”后,DBX 会创建持久化密钥;如果系统弹出凭据访问授权窗口,请允许访问。",
keyRecoveryHint: "请恢复该数据目录原有的密钥,或配置用于加密现有数据的同一外部密钥。不要使用新密钥替换原密钥。",
progress: "正在创建备份、迁移并验证,请勿关闭 DBX…",
failed: "迁移没有完成,原始数据已保留。",
+2
View File
@@ -10485,6 +10485,8 @@ export default withEnglishFallback({
verifyAdvice: "驗證成功前請保留備份。",
},
launching: "正在啟動 DBX…",
keyWillCreate: "首次遷移時建立",
keyWillCreateHint: "目前裝置還沒有 DBX 本機加密金鑰。點選「開始遷移」後,DBX 會建立持久化金鑰;如果系統彈出認證存取授權視窗,請允許存取。",
},
solrAdmin: {
title: "Solr 管理",
@@ -1,9 +1,11 @@
export type MigrationState = "pending" | "running" | "succeeded" | "failed" | "not_required";
export type MigrationKeyStatus = "ready" | "will_create" | "unavailable" | "invalid" | "mismatch" | "missing_for_ciphertext";
export interface MigrationPreflight {
migrationId: string;
state: MigrationState;
needsMigration: boolean;
keyProviderAvailable: boolean;
keyStatus?: MigrationKeyStatus;
keyCreationAllowed?: boolean;
databasePlaintextCount: number;
connectionCount: number;
+22
View File
@@ -118,6 +118,28 @@ describe("migration store", () => {
expect(store.state.errorCode).toBe("MISSING_MANAGED_KEY");
});
it("treats an absent platform key as actionable setup before migration", async () => {
const store = useMigrationStore({
migrationStatus: vi.fn().mockResolvedValue({
...pending(),
keyProviderAvailable: false,
keyCreationAllowed: true,
keyStatus: "will_create",
errorCode: "KEY_PROVIDER_UNAVAILABLE",
errorMessage: "The local secret provider is unavailable",
}),
migrationStart: vi.fn(),
migrationRetry: vi.fn(),
migrationCleanupBackups: vi.fn(),
});
await store.initialize();
expect(store.state.error).toBeNull();
expect(store.state.errorCode).toBe("KEY_PROVIDER_UNAVAILABLE");
expect(store.diagnostic().keyStatus).toBe("will_create");
});
it("keeps completion through a failed cleanup and successful cleanup retry", async () => {
let finishCleanup!: () => void;
const cleanup = vi
+2 -1
View File
@@ -22,7 +22,7 @@ export function useMigrationStore(backend: MigrationApi = api) {
// A managed data-directory key is intentionally created when migration
// starts. Its absence during the read-only preflight is actionable, not a
// migration failure, as long as the backend explicitly allows creation.
const hasBlockingError = () => Boolean(state.errorCode && !(state.errorCode === "MISSING_MANAGED_KEY" && state.status?.keyCreationAllowed === true));
const hasBlockingError = () => Boolean(state.errorCode && state.status?.keyStatus !== "will_create" && !(state.errorCode === "MISSING_MANAGED_KEY" && state.status?.keyCreationAllowed === true));
// A retained backup is a recovery asset and must not make the success page
// reappear on later launches. An attempt in this session keeps the wizard
// visible even when its response is lost, until the user enters the app.
@@ -106,6 +106,7 @@ export function useMigrationStore(backend: MigrationApi = api) {
state: s?.state,
counts: s ? { connections: s.connectionCount, databasePlaintext: s.databasePlaintextCount, plugin: s.pluginSecretCount, ai: s.aiSecretCount, tunnel: s.tunnelSecretCount, sync: s.syncCredentialCount } : {},
keyProviderAvailable: s?.keyProviderAvailable,
keyStatus: s?.keyStatus,
backupPath: s?.backupPath,
errorCode: state.errorCode,
};