You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1372 lines
50 KiB
1372 lines
50 KiB
<template>
|
|
<div class="min-h-screen bg-gray-50 py-8">
|
|
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
|
<!-- Header -->
|
|
<div>
|
|
<h1 class="text-3xl font-bold text-gray-900">Database Migration</h1>
|
|
<p class="mt-2 text-gray-600">
|
|
Compare and migrate data between Dexie (IndexedDB) and SQLite
|
|
databases
|
|
</p>
|
|
<p class="mt-2 text-gray-600">
|
|
First, we recommend you hit these first two buttons to get your backups, just in case!
|
|
If anything doesn't look right when you're done, Trent will help with... anything and everything. 😁
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Migration Options -->
|
|
<!--
|
|
<div class="mt-4">
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Migration Options
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div class="flex items-center">
|
|
<input
|
|
id="overwrite-existing"
|
|
v-model="overwriteExisting"
|
|
type="checkbox"
|
|
class="h-4 w-4 text-blue-600 focus:ring-blue-500 border-gray-300 rounded"
|
|
/>
|
|
<label
|
|
for="overwrite-existing"
|
|
class="ml-2 block text-sm text-gray-900"
|
|
>
|
|
Overwrite existing records in SQLite
|
|
</label>
|
|
</div>
|
|
|
|
<p class="text-sm text-gray-600">
|
|
When enabled, existing records in SQLite will be updated with
|
|
data from Dexie. When disabled, existing records will be skipped
|
|
during migration.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
-->
|
|
|
|
<div
|
|
class="text-lg leading-6 font-medium text-red-900 mt-4"
|
|
>
|
|
<p
|
|
v-if="comparison && (comparison.sqliteAccounts.length > 1 || comparison.sqliteSettings.length > 1 || comparison.sqliteContacts.length > 0)"
|
|
>
|
|
Beware: you have unexpected existing data in the new database that will be overwritten. Talk with Trent.
|
|
</p>
|
|
<p
|
|
v-if="cannotfindMainAccount"
|
|
>
|
|
We cannot find your main account. Talk with Trent.
|
|
</p>
|
|
<p
|
|
v-if="hasMultipleMnemonics"
|
|
>
|
|
You have multiple accounts. If you didn't mean to, talk with Trent.
|
|
</p>
|
|
</div>
|
|
<div>
|
|
<p v-if="downloadMnemonic" class="text-green-500">
|
|
Here is your seed for account {{ downloadMnemonicAddress?.substring('did:ethr:0x'.length).substring(0, 3) }} -- write it down!
|
|
<br />
|
|
{{ downloadMnemonic }}
|
|
</p>
|
|
</div>
|
|
|
|
<!-- Action Buttons -->
|
|
<div class="mt-4 mb-8 flex flex-wrap gap-4">
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="downloadAccount"
|
|
>
|
|
<IconRenderer
|
|
v-if="isLoading"
|
|
icon-name="spinner"
|
|
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
fill="currentColor"
|
|
/>
|
|
<IconRenderer
|
|
v-else
|
|
icon-name="chart"
|
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
|
/>
|
|
Download Account
|
|
</button>
|
|
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="downloadSettingsContacts"
|
|
>
|
|
<IconRenderer
|
|
v-if="isLoading"
|
|
icon-name="spinner"
|
|
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
fill="currentColor"
|
|
/>
|
|
<IconRenderer
|
|
v-else
|
|
icon-name="chart"
|
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
|
/>
|
|
Download Settings & Contacts
|
|
</button>
|
|
|
|
<div>
|
|
<a ref="downloadLink">
|
|
<!-- No content necessary -->
|
|
<p
|
|
v-if="downloadSettingsContactsBlob"
|
|
class="block w-full text-center text-md bg-gradient-to-b from-green-500 to-green-800 shadow-[inset_0_-1px_0_0_rgba(0,0,0,0.5)] text-white px-1.5 py-2 rounded-md mb-6"
|
|
>
|
|
If no download happened yet, click here to download now.
|
|
</p>
|
|
</a>
|
|
</div>
|
|
|
|
<div class="w-full border-t border-gray-200 my-4"></div>
|
|
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="compareDatabases"
|
|
>
|
|
<IconRenderer
|
|
v-if="isLoading"
|
|
icon-name="spinner"
|
|
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
fill="currentColor"
|
|
/>
|
|
<IconRenderer
|
|
v-else
|
|
icon-name="chart"
|
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
|
/>
|
|
Compare Databases
|
|
</button>
|
|
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-6 py-3 border border-transparent text-base font-medium rounded-md shadow-sm text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="migrateAll"
|
|
>
|
|
<IconRenderer
|
|
v-if="isLoading"
|
|
icon-name="spinner"
|
|
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
fill="currentColor"
|
|
/>
|
|
<IconRenderer
|
|
v-else
|
|
icon-name="check"
|
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
|
/>
|
|
Migrate All
|
|
</button>
|
|
|
|
<div class="w-full border-t border-gray-200 my-4"></div>
|
|
|
|
<!-- Error State -->
|
|
<div
|
|
v-if="error"
|
|
class="mb-6 bg-red-50 border border-red-200 rounded-lg p-4"
|
|
>
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="warning"
|
|
svg-class="h-5 w-5 text-red-400"
|
|
fill="currentColor"
|
|
/>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-red-800">Error</h3>
|
|
<div class="mt-2 text-sm text-red-700">
|
|
<p>{{ error }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Success State -->
|
|
<div
|
|
v-if="successMessage"
|
|
class="mb-6 bg-green-50 border border-green-200 rounded-lg p-4"
|
|
>
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-5 w-5 text-green-400"
|
|
/>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-green-800">Success</h3>
|
|
<div class="mt-2 text-sm text-green-700">
|
|
<p>{{ successMessage }}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="w-full border-t border-gray-200 my-4"></div>
|
|
|
|
<button
|
|
class="inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="exportComparison"
|
|
>
|
|
<IconRenderer icon-name="download" svg-class="-ml-1 mr-3 h-5 w-5" />
|
|
Export Comparison
|
|
</button>
|
|
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="displayDatabases"
|
|
>
|
|
<IconRenderer
|
|
v-if="isLoading"
|
|
icon-name="spinner"
|
|
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
fill="currentColor"
|
|
/>
|
|
<IconRenderer
|
|
v-else
|
|
icon-name="chart"
|
|
svg-class="-ml-1 mr-3 h-5 w-5"
|
|
/>
|
|
Show Previous Data
|
|
</button>
|
|
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-orange-600 hover:bg-orange-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="migrateAccounts"
|
|
>
|
|
<IconRenderer icon-name="lock" svg-class="-ml-1 mr-3 h-5 w-5" />
|
|
Migrate Accounts
|
|
</button>
|
|
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-purple-600 hover:bg-purple-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-purple-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="migrateSettings"
|
|
>
|
|
<IconRenderer icon-name="settings" svg-class="-ml-1 mr-3 h-5 w-5" />
|
|
Migrate Settings
|
|
</button>
|
|
|
|
<button
|
|
:disabled="isLoading"
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
@click="migrateContacts"
|
|
>
|
|
<IconRenderer icon-name="plus" svg-class="-ml-1 mr-3 h-5 w-5" />
|
|
Migrate Contacts
|
|
</button>
|
|
</div>
|
|
|
|
<div class="mt-4">
|
|
<a
|
|
class="inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
|
href="/"
|
|
>
|
|
Take me back to the home page
|
|
</a>
|
|
</div>
|
|
|
|
<!-- Migration Information -->
|
|
<div
|
|
v-if="comparison"
|
|
class="mt-2 mb-6 bg-blue-50 border border-blue-200 rounded-lg p-4"
|
|
>
|
|
<div class="flex">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="info"
|
|
svg-class="h-5 w-5 text-blue-400"
|
|
fill="currentColor"
|
|
/>
|
|
</div>
|
|
<div class="ml-3">
|
|
<h3 class="text-sm font-medium text-blue-800">
|
|
Migration Order & Recommendations
|
|
</h3>
|
|
<div class="mt-2 text-sm text-blue-700">
|
|
<p class="mb-2">
|
|
<strong>Recommended:</strong> Use "Migrate All" to ensure proper
|
|
data integrity and avoid foreign key issues.
|
|
</p>
|
|
<p class="mb-2">
|
|
<strong>Migration Order:</strong> Accounts → Settings → Contacts
|
|
</p>
|
|
<ul class="list-disc list-inside space-y-1">
|
|
<li>
|
|
<strong>Accounts:</strong> Foundation data containing DIDs
|
|
</li>
|
|
<li>
|
|
<strong>Settings:</strong> References accountDid and activeDid
|
|
from accounts
|
|
</li>
|
|
<li>
|
|
<strong>Contacts:</strong> Independent data, migrated last for
|
|
consistency
|
|
</li>
|
|
</ul>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
<div v-if="isLoading" class="text-center py-12">
|
|
<div
|
|
class="inline-flex items-center px-4 py-2 font-semibold leading-6 text-sm shadow rounded-md text-white bg-blue-500 hover:bg-blue-400 transition ease-in-out duration-150 cursor-not-allowed"
|
|
>
|
|
<IconRenderer
|
|
icon-name="spinner"
|
|
svg-class="animate-spin -ml-1 mr-3 h-5 w-5 text-white"
|
|
fill="currentColor"
|
|
/>
|
|
{{ loadingMessage }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Comparison Results -->
|
|
<div v-if="comparison" class="space-y-6">
|
|
<!-- Summary Cards -->
|
|
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-6 gap-6">
|
|
<!-- Accounts Summary (moved first) -->
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="lock"
|
|
svg-class="h-6 w-6 text-orange-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Dexie Accounts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.dexieAccounts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-6 w-6 text-teal-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
SQLite Accounts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.sqliteAccounts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Summary (second) -->
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="settings"
|
|
svg-class="h-6 w-6 text-purple-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Dexie Settings
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.dexieSettings.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-6 w-6 text-indigo-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
SQLite Settings
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.sqliteSettings.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contacts Summary (last) -->
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="chart"
|
|
svg-class="h-6 w-6 text-blue-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
Dexie Contacts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.dexieContacts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white overflow-hidden shadow rounded-lg">
|
|
<div class="p-5">
|
|
<div class="flex items-center">
|
|
<div class="flex-shrink-0">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-6 w-6 text-green-600"
|
|
/>
|
|
</div>
|
|
<div class="ml-5 w-0 flex-1">
|
|
<dl>
|
|
<dt class="text-sm font-medium text-gray-500 truncate">
|
|
SQLite Contacts
|
|
</dt>
|
|
<dd class="text-lg font-medium text-gray-900">
|
|
{{ comparison.sqliteContacts.length }}
|
|
</dd>
|
|
</dl>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Differences Section -->
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
|
<!-- Account Differences (moved first) -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Account Differences
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="plusCircle"
|
|
svg-class="h-5 w-5 text-blue-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-blue-900">Add</span>
|
|
</div>
|
|
<span class="text-sm font-bold text-blue-900">{{
|
|
comparison.differences.accounts.added.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Unmodified</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.accounts.unmodified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="trash"
|
|
svg-class="h-5 w-5 text-red-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-red-900"
|
|
>Keep</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-red-900">{{
|
|
comparison.differences.accounts.missing.length
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Account Details -->
|
|
<div
|
|
v-if="comparison.differences.accounts.added.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Add Accounts ({{ comparison.differences.accounts.added.length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="account in comparison.differences.accounts.added"
|
|
:key="account.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">ID: {{ account.id }}</div>
|
|
<div class="text-gray-500">{{ account.did }}</div>
|
|
<div class="text-gray-400">Created: {{ account.dateCreated }}</div>
|
|
<div class="text-gray-400">Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}</div>
|
|
<div class="text-gray-400">Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unmodified Accounts -->
|
|
<div
|
|
v-if="comparison.differences.accounts.unmodified.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Unmodified Accounts ({{ comparison.differences.accounts.unmodified.length }}):
|
|
</h4>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="account in comparison.differences.accounts.unmodified"
|
|
:key="account.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">ID: {{ account.id }}</div>
|
|
<div class="text-gray-500">{{ account.did }}</div>
|
|
<div class="text-gray-400">Created: {{ account.dateCreated }}</div>
|
|
<div class="text-gray-400">Has Identity: {{ getAccountHasIdentity(account) ? 'Yes' : 'No' }}</div>
|
|
<div class="text-gray-400">Has Mnemonic: {{ getAccountHasMnemonic(account) ? 'Yes' : 'No' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Keep Accounts -->
|
|
<div
|
|
v-if="comparison.differences.accounts.missing.filter(a => a).length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Keep Accounts ({{ comparison.differences.accounts.missing.filter(a => a).length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="did in comparison.differences.accounts.missing.filter(a => a)"
|
|
:key="did"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="text-gray-500">{{ did }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Differences (second) -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Settings Differences
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="plusCircle"
|
|
svg-class="h-5 w-5 text-blue-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-blue-900">Add</span>
|
|
</div>
|
|
<span class="text-sm font-bold text-blue-900">{{
|
|
comparison.differences.settings.added.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="edit"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Modify</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.settings.modified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Unmodified</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.settings.unmodified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="trash"
|
|
svg-class="h-5 w-5 text-red-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-red-900"
|
|
>Keep</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-red-900">{{
|
|
comparison.differences.settings.missing.length
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Settings Details -->
|
|
<div
|
|
v-if="comparison.differences.settings.added.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Add Settings ({{ comparison.differences.settings.added.length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="setting in comparison.differences.settings.added"
|
|
:key="setting.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ getSettingDisplayName(setting) }}</div>
|
|
<div class="text-gray-500">ID: {{ setting.id }}</div>
|
|
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modify Settings -->
|
|
<div
|
|
v-if="comparison.differences.settings.modified.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Modify Settings ({{ comparison.differences.settings.modified.length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="setting in comparison.differences.settings.modified"
|
|
:key="setting.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ getSettingDisplayName(setting) }}</div>
|
|
<div class="text-gray-500">ID: {{ setting.id }}</div>
|
|
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unmodified Settings -->
|
|
<div
|
|
v-if="comparison.differences.settings.unmodified.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Unmodified Settings ({{ comparison.differences.settings.unmodified.length }}):
|
|
</h4>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="setting in comparison.differences.settings.unmodified"
|
|
:key="setting.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ getSettingDisplayName(setting) }}</div>
|
|
<div class="text-gray-500">ID: {{ setting.id }}</div>
|
|
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Keep Settings -->
|
|
<div
|
|
v-if="comparison.differences.settings.missing.filter(s => s.accountDid || s.activeDid).length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Keep Settings ({{ comparison.differences.settings.missing.filter(s => s.accountDid || s.activeDid).length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="setting in comparison.differences.settings.missing.filter(s => s.accountDid || s.activeDid)"
|
|
:key="setting.id"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ getSettingDisplayName(setting) }}</div>
|
|
<div class="text-gray-500">ID: {{ setting.id }}</div>
|
|
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contacts Differences (last) -->
|
|
<div class="bg-white shadow rounded-lg">
|
|
<div class="px-4 py-5 sm:p-6">
|
|
<h3 class="text-lg leading-6 font-medium text-gray-900 mb-4">
|
|
Contact Differences
|
|
</h3>
|
|
|
|
<div class="space-y-4">
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-blue-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="plusCircle"
|
|
svg-class="h-5 w-5 text-blue-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-blue-900">Add</span>
|
|
</div>
|
|
<span class="text-sm font-bold text-blue-900">{{
|
|
comparison.differences.contacts.added.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="edit"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Modify</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.contacts.modified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-yellow-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="check"
|
|
svg-class="h-5 w-5 text-yellow-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-yellow-900"
|
|
>Unmodified</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-yellow-900">{{
|
|
comparison.differences.contacts.unmodified.length
|
|
}}</span>
|
|
</div>
|
|
|
|
<div
|
|
class="flex items-center justify-between p-3 bg-red-50 rounded-lg"
|
|
>
|
|
<div class="flex items-center">
|
|
<IconRenderer
|
|
icon-name="trash"
|
|
svg-class="h-5 w-5 text-red-600 mr-2"
|
|
/>
|
|
<span class="text-sm font-medium text-red-900"
|
|
>Keep</span
|
|
>
|
|
</div>
|
|
<span class="text-sm font-bold text-red-900">{{
|
|
comparison.differences.contacts.missing.length
|
|
}}</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Contact Details -->
|
|
<div
|
|
v-if="comparison.differences.contacts.added.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Add Contacts ({{ comparison.differences.contacts.added.length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="contact in comparison.differences.contacts.added"
|
|
:key="contact.did"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ contact.name || '<empty>' }}</div>
|
|
<div class="text-gray-500">{{ contact.did }}</div>
|
|
<div class="text-gray-400">{{ contact.contactMethods?.length || 0 }} contact methods</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Modify Contacts -->
|
|
<div
|
|
v-if="comparison.differences.contacts.modified.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Modify Contacts ({{ comparison.differences.contacts.modified.length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="contact in comparison.differences.contacts.modified"
|
|
:key="contact.did"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ contact.name || '<empty>' }}</div>
|
|
<div class="text-gray-500">{{ contact.did }}</div>
|
|
<div class="text-gray-400">{{ contact.contactMethods?.length || 0 }} contact methods</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Unmodified Contacts -->
|
|
<div
|
|
v-if="comparison.differences.contacts.unmodified.length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Unmodified Contacts ({{ comparison.differences.contacts.unmodified.length }}):
|
|
</h4>
|
|
</div>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="contact in comparison.differences.contacts.unmodified"
|
|
:key="contact.did"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ contact.name || '<empty>' }}</div>
|
|
<div class="text-gray-500">{{ contact.did }}</div>
|
|
<div class="text-gray-400">{{ contact.contactMethods?.length || 0 }} contact methods</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Keep Contacts -->
|
|
<div
|
|
v-if="comparison.differences.contacts.missing.filter(c => c.did).length > 0"
|
|
class="mt-4"
|
|
>
|
|
<h4 class="text-sm font-medium text-gray-900 mb-2">
|
|
Keep Contacts ({{ comparison.differences.contacts.missing.filter(c => c.did).length }}):
|
|
</h4>
|
|
<div class="space-y-1">
|
|
<div
|
|
v-for="contact in comparison.differences.contacts.missing.filter(c => c.did)"
|
|
:key="contact.did"
|
|
class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
|
|
>
|
|
<div class="font-medium">{{ contact.name || '<empty>' }}</div>
|
|
<div class="text-gray-500">{{ contact.did }}</div>
|
|
<div class="text-gray-400">{{ contact.contactMethods?.length || 0 }} contact methods</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Export Results -->
|
|
<div v-if="exportedData" class="mt-4 space-y-6">
|
|
<h2>Exported Data</h2>
|
|
<span
|
|
v-on:click="copyToClipboard"
|
|
class="text-blue-500 cursor-pointer hover:text-blue-700"
|
|
>
|
|
Copy to Clipboard
|
|
</span>
|
|
<pre>{{ JSON.stringify(exportedData, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts">
|
|
import { Component, Vue } from "vue-facing-decorator";
|
|
import { useClipboard } from "@vueuse/core";
|
|
import { Router } from "vue-router";
|
|
|
|
import IconRenderer from "../components/IconRenderer.vue";
|
|
import {
|
|
compareDatabases,
|
|
migrateSettings,
|
|
migrateAccounts,
|
|
migrateAll,
|
|
generateComparisonYaml,
|
|
type DataComparison,
|
|
type MigrationResult,
|
|
getDexieAccounts,
|
|
getDexieSettings,
|
|
getDexieContacts,
|
|
getDexieExportBlob,
|
|
} from "../services/indexedDBMigrationService";
|
|
import { logger } from "../utils/logger";
|
|
|
|
/**
|
|
* Database Migration View Component
|
|
*
|
|
* This component provides a user interface for comparing and migrating data
|
|
* between Dexie (IndexedDB) and SQLite databases. It allows users to:
|
|
*
|
|
* 1. Compare data between the two databases
|
|
* 2. View differences in contacts and settings
|
|
* 3. Migrate contacts from Dexie to SQLite
|
|
* 4. Migrate settings from Dexie to SQLite
|
|
* 5. Export comparison results
|
|
*
|
|
* The component includes comprehensive error handling, loading states,
|
|
* and user-friendly feedback for all operations.
|
|
*
|
|
* @author Matthew Raymer
|
|
* @version 1.0.0
|
|
* @since 2024
|
|
*/
|
|
@Component({
|
|
name: "DatabaseMigration",
|
|
components: {
|
|
IconRenderer,
|
|
},
|
|
})
|
|
export default class DatabaseMigration extends Vue {
|
|
$router!: Router;
|
|
|
|
// Component state
|
|
private comparison: DataComparison | null = null;
|
|
private cannotfindMainAccount = false;
|
|
private downloadSettingsContactsBlob?: Blob;
|
|
private downloadMnemonic?: string;
|
|
private downloadMnemonicAddress?: string;
|
|
private hasMultipleMnemonics = false;
|
|
private isLoading = false;
|
|
private loadingMessage = "";
|
|
private error = "";
|
|
private exportedData: Record<string, any> | null = null;
|
|
private successMessage = "";
|
|
|
|
useClipboard = useClipboard;
|
|
|
|
/**
|
|
* Computed property to get the display name for a setting
|
|
* Handles both live comparison data and exported JSON format
|
|
*
|
|
* @param {any} setting - The setting object
|
|
* @returns {string} The display name for the setting
|
|
*/
|
|
getSettingDisplayName(setting: any): string {
|
|
// Handle exported JSON format (has 'type' and 'did' fields)
|
|
if (setting.type && setting.did) {
|
|
return `${setting.type} (${setting.did})`;
|
|
}
|
|
|
|
// Handle live comparison data (has 'activeDid' or 'accountDid' fields)
|
|
const did = setting.activeDid || setting.accountDid;
|
|
const type = setting.id === 1 ? 'master' : 'account';
|
|
return `${type} (${did || 'no DID'})`;
|
|
}
|
|
|
|
/**
|
|
* Computed property to check if an account has identity
|
|
* Handles both live comparison data and exported JSON format
|
|
*
|
|
* @param {any} account - The account object
|
|
* @returns {boolean} True if account has identity
|
|
*/
|
|
getAccountHasIdentity(account: any): boolean {
|
|
// Handle exported JSON format (has 'hasIdentity' field)
|
|
if (account.hasIdentity !== undefined) {
|
|
return account.hasIdentity;
|
|
}
|
|
|
|
// Handle live comparison data (has 'identity' field)
|
|
return !!account.identity;
|
|
}
|
|
|
|
/**
|
|
* Computed property to check if an account has mnemonic
|
|
* Handles both live comparison data and exported JSON format
|
|
*
|
|
* @param {any} account - The account object
|
|
* @returns {boolean} True if account has mnemonic
|
|
*/
|
|
getAccountHasMnemonic(account: any): boolean {
|
|
// Handle exported JSON format (has 'hasMnemonic' field)
|
|
if (account.hasMnemonic !== undefined) {
|
|
return account.hasMnemonic;
|
|
}
|
|
|
|
// Handle live comparison data (has 'mnemonic' field)
|
|
return !!account.mnemonic;
|
|
}
|
|
|
|
/**
|
|
* Copies exported data to clipboard and shows success message
|
|
*/
|
|
async copyToClipboard(): Promise<void> {
|
|
if (!this.exportedData) return;
|
|
|
|
try {
|
|
await this.useClipboard().copy(JSON.stringify(this.exportedData, null, 2));
|
|
// Use global window object properly
|
|
if (typeof window !== 'undefined') {
|
|
window.alert('Copied to clipboard!');
|
|
}
|
|
} catch (error) {
|
|
logger.error('Failed to copy to clipboard:', error);
|
|
if (typeof window !== 'undefined') {
|
|
window.alert('Failed to copy to clipboard');
|
|
}
|
|
}
|
|
}
|
|
|
|
async downloadAccount() {
|
|
const accounts = await getDexieAccounts();
|
|
this.hasMultipleMnemonics = accounts.length > 1;
|
|
const settings = await getDexieSettings();
|
|
let primaryAccount;
|
|
if (settings.length > 0) {
|
|
const primaryDid = settings[0].activeDid;
|
|
primaryAccount = accounts.find(acc => acc.did === primaryDid);
|
|
if (!primaryAccount) {
|
|
this.cannotfindMainAccount = true;
|
|
// abort everything
|
|
}
|
|
} else {
|
|
// should never happen
|
|
this.cannotfindMainAccount = true;
|
|
}
|
|
if (primaryAccount) {
|
|
this.downloadMnemonic = primaryAccount.mnemonic;
|
|
this.downloadMnemonicAddress = primaryAccount.did;
|
|
}
|
|
}
|
|
|
|
async downloadSettingsContacts() {
|
|
this.downloadSettingsContactsBlob = await getDexieExportBlob();
|
|
const downloadUrl = URL.createObjectURL(this.downloadSettingsContactsBlob);
|
|
const downloadAnchor = this.$refs.downloadLink as HTMLAnchorElement;
|
|
downloadAnchor.href = downloadUrl;
|
|
downloadAnchor.download = `TimeSafari-settings-contacts-backup.json`;
|
|
downloadAnchor.click(); // doesn't work for some browsers, eg. DuckDuckGo
|
|
}
|
|
|
|
async displayDatabases() {
|
|
this.exportedData = {
|
|
accounts: await getDexieAccounts(),
|
|
settings: await getDexieSettings(),
|
|
contacts: await getDexieContacts()
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Migrates all data from Dexie to SQLite in the proper order
|
|
*
|
|
* This method performs a complete migration of all data from Dexie to SQLite
|
|
* in the correct order to avoid foreign key constraint issues:
|
|
* 1. Accounts (foundational - contains DIDs)
|
|
* 2. Settings (references accountDid, activeDid)
|
|
* 3. Contacts (independent, but migrated after accounts for consistency)
|
|
*
|
|
* This is the recommended approach as it ensures data integrity and
|
|
* handles all foreign key relationships automatically.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async migrateAll(): Promise<void> {
|
|
this.setLoading("Migrating all data (Accounts → Settings → Contacts)...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
const result: MigrationResult = await migrateAll();
|
|
|
|
if (result.success) {
|
|
const totalMigrated =
|
|
result.accountsMigrated +
|
|
result.settingsMigrated +
|
|
result.contactsMigrated;
|
|
this.successMessage = `Successfully migrated ${totalMigrated} total records: ${result.accountsMigrated} accounts, ${result.settingsMigrated} settings, ${result.contactsMigrated} contacts.`;
|
|
if (result.warnings.length > 0) {
|
|
this.successMessage += ` ${result.warnings.length} warnings.`;
|
|
}
|
|
this.successMessage += " Now finish by migrating contacts.";
|
|
logger.info(
|
|
"[DatabaseMigration] Complete migration successful",
|
|
result,
|
|
);
|
|
|
|
// Refresh comparison data after successful migration
|
|
this.comparison = await compareDatabases();
|
|
} else {
|
|
this.error = `Migration failed: ${result.errors.join(", ")}`;
|
|
logger.error(
|
|
"[DatabaseMigration] Complete migration failed:",
|
|
result.errors,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Failed to migrate all data: ${error}`;
|
|
logger.error("[DatabaseMigration] Complete migration failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compares data between Dexie and SQLite databases
|
|
*
|
|
* This method retrieves data from both databases and identifies
|
|
* differences. It provides comprehensive feedback and error handling.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async compareDatabases(): Promise<void> {
|
|
this.setLoading("Comparing databases...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
this.comparison = await compareDatabases();
|
|
const totalItems =
|
|
this.comparison.differences.contacts.added.length +
|
|
this.comparison.differences.settings.added.length +
|
|
this.comparison.differences.accounts.added.length;
|
|
this.successMessage = `Comparison completed successfully. Found ${totalItems} items to migrate.`;
|
|
logger.info(
|
|
"[DatabaseMigration] Database comparison completed successfully",
|
|
);
|
|
} catch (error) {
|
|
this.error = `Failed to compare databases: ${error}`;
|
|
logger.error("[DatabaseMigration] Database comparison failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates contacts from Dexie to SQLite database
|
|
*
|
|
* This method transfers contacts from the Dexie database to SQLite.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async migrateContacts(): Promise<void> {
|
|
// load all contacts from indexedDB
|
|
const dexieContacts = await getDexieContacts();
|
|
// now reroute to the contact import view with query parameter of contacts
|
|
this.$router.push({
|
|
name: "contact-import",
|
|
query: {
|
|
contacts: JSON.stringify(dexieContacts),
|
|
},
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Migrates settings from Dexie to SQLite database
|
|
*
|
|
* This method transfers settings from the Dexie database to SQLite.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async migrateSettings(): Promise<void> {
|
|
this.setLoading("Migrating settings...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
const result: MigrationResult = await migrateSettings();
|
|
|
|
if (result.success) {
|
|
this.successMessage = `Successfully migrated ${result.settingsMigrated} settings.`;
|
|
if (result.warnings.length > 0) {
|
|
this.successMessage += ` ${result.warnings.length} warnings.`;
|
|
}
|
|
logger.info(
|
|
"[DatabaseMigration] Settings migration completed successfully",
|
|
result,
|
|
);
|
|
|
|
// Refresh comparison data after successful migration
|
|
this.comparison = await compareDatabases();
|
|
} else {
|
|
this.error = `Migration failed: ${result.errors.join(", ")}`;
|
|
logger.error(
|
|
"[DatabaseMigration] Settings migration failed:",
|
|
result.errors,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Failed to migrate settings: ${error}`;
|
|
logger.error("[DatabaseMigration] Settings migration failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Migrates accounts from Dexie to SQLite database
|
|
*
|
|
* This method transfers accounts from the Dexie database to SQLite,
|
|
* For accounts with mnemonic data, it uses the importFromMnemonic utility for proper key derivation.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async migrateAccounts(): Promise<void> {
|
|
this.setLoading("Migrating accounts...");
|
|
this.clearMessages();
|
|
|
|
try {
|
|
const result: MigrationResult = await migrateAccounts();
|
|
|
|
if (result.success) {
|
|
this.successMessage = `Successfully migrated ${result.accountsMigrated} accounts.`;
|
|
if (result.warnings.length > 0) {
|
|
this.successMessage += ` ${result.warnings.length} warnings.`;
|
|
}
|
|
logger.info(
|
|
"[DatabaseMigration] Account migration completed successfully",
|
|
result,
|
|
);
|
|
|
|
// Refresh comparison data after successful migration
|
|
this.comparison = await compareDatabases();
|
|
} else {
|
|
this.error = `Migration failed: ${result.errors.join(", ")}`;
|
|
logger.error(
|
|
"[DatabaseMigration] Account migration failed:",
|
|
result.errors,
|
|
);
|
|
}
|
|
} catch (error) {
|
|
this.error = `Failed to migrate accounts: ${error}`;
|
|
logger.error("[DatabaseMigration] Account migration failed:", error);
|
|
} finally {
|
|
this.setLoading("");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Exports the comparison data to a JSON file
|
|
*
|
|
* This method generates a JSON file containing the complete comparison
|
|
* data in a format that matches the exported JSON structure.
|
|
*
|
|
* @async
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async exportComparison(): Promise<void> {
|
|
if (!this.comparison) {
|
|
this.error = "No comparison data available to export";
|
|
return;
|
|
}
|
|
|
|
try {
|
|
const yamlData = generateComparisonYaml(this.comparison);
|
|
const blob = new Blob([yamlData], { type: "application/json" });
|
|
const url = URL.createObjectURL(blob);
|
|
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `database-comparison-${new Date().toISOString().split('T')[0]}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
|
|
this.successMessage = "Comparison data exported successfully";
|
|
logger.info("[DatabaseMigration] Comparison data exported successfully");
|
|
} catch (error) {
|
|
this.error = `Failed to export comparison data: ${error}`;
|
|
logger.error("[DatabaseMigration] Export failed:", error);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Sets the loading state and message
|
|
*
|
|
* @param {string} message - The loading message to display
|
|
*/
|
|
private setLoading(message: string): void {
|
|
this.isLoading = message !== "";
|
|
this.loadingMessage = message;
|
|
}
|
|
|
|
/**
|
|
* Clears all error and success messages
|
|
*/
|
|
private clearMessages(): void {
|
|
this.error = "";
|
|
this.successMessage = "";
|
|
}
|
|
}
|
|
</script>
|
|
|