import { computed, reactive, ref } from "vue"; import * as api from "@/lib/backend/api"; import type { MigrationPreflight, MigrationReport } from "@/lib/backend/migration"; type MigrationApi = Pick; // Each startup boundary owns its state; an action failure never erases its report. export function useMigrationStore(backend: MigrationApi = api) { const state = reactive<{ status: MigrationPreflight | null; report: MigrationReport | null; loading: boolean; busy: boolean; error: "statusFailed" | "migrationFailed" | "cleanupFailed" | null; errorCode: string | null; errorMessage: string | null; entered: boolean; step: 1 | 2 | 3; }>({ status: null, report: null, loading: true, busy: false, error: null, errorCode: null, errorMessage: null, entered: false, step: 1 }); const migrationAttempted = ref(false); const completed = computed(() => state.status !== null && !state.status.needsMigration && ["succeeded", "not_required"].includes(state.status.state)); // 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.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. const blocking = computed(() => state.loading || state.busy || state.error === "statusFailed" || !completed.value || (!state.entered && (migrationAttempted.value || Boolean(state.report)))); function clearError() { state.error = null; state.errorCode = null; state.errorMessage = null; } async function refreshStatus() { // Only this endpoint returns classified, redacted migration errors. Transport // exceptions may contain configuration values and must never enter UI state. state.status = await backend.migrationStatus(); state.errorCode = state.status.errorCode ?? null; state.errorMessage = state.status.errorMessage ?? null; } async function initialize() { state.loading = true; clearError(); try { await refreshStatus(); state.step = completed.value ? 3 : 1; // A completed migration only needs the success page in the session that // performed it. On later launches, retained backups are recovery assets, // not a reason to block the normal application startup. `entered` is // therefore only changed by the explicit success-page action below. state.entered = false; if (state.status?.state === "failed" || hasBlockingError()) state.error = "migrationFailed"; } catch { state.error = "statusFailed"; } finally { state.loading = false; } } async function run(retry: boolean) { if (state.busy) return; state.busy = true; migrationAttempted.value = true; state.step = 2; clearError(); try { state.report = await (retry ? backend.migrationRetry() : backend.migrationStart()); } catch { state.error = "migrationFailed"; } try { await refreshStatus(); if (completed.value) { state.step = 3; state.error = null; } if (!completed.value || hasBlockingError()) state.error = "migrationFailed"; } catch { state.error = "statusFailed"; } finally { state.busy = false; } } async function cleanup() { if (state.busy) return; state.busy = true; clearError(); try { await backend.migrationCleanupBackups(); if (state.status) state.status.backupPath = null; if (state.report) state.report.backupPath = null; } catch { state.error = "cleanupFailed"; } finally { state.busy = false; } } function diagnostic() { const s = state.status; return { migrationId: s?.migrationId, actionError: state.error, step: state.step, needsMigration: s?.needsMigration, legacyJsonFiles: s?.legacyJsonFiles, 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, }; } function enter() { if (completed.value && !state.busy) state.entered = true; } return { state, blocking, completed, initialize, start: () => run(false), retry: () => run(true), cleanup, diagnostic, enter }; }