Browse Source

feat: improve database migration comparison and UI display

- Enhanced migration service to filter empty/default records from comparisons
- Updated comparison output to exclude records without meaningful data
- Improved UI display with better visual hierarchy and count indicators
- Added security improvements to prevent sensitive data exposure
- Fixed settings migration to properly handle MASTER_SETTINGS_KEY vs account DIDs
- Updated verification display to show filtered counts and detailed differences
- Improved data formatting for contacts, settings, and accounts sections
- Added proper filtering for missing records to avoid counting empty entries

Changes:
- Filter SQLite records to only include those with actual DIDs/data
- Update comparison counts to reflect meaningful differences only
- Enhance UI with count indicators and better visual organization
- Replace sensitive data display with boolean flags (hasIdentity, hasMnemonic)
- Fix settings migration logic for proper DID field handling
- Improve verification message to show detailed breakdown by type
- Add proper filtering for missing records in all data types

Security: Prevents exposure of mnemonics, private keys, and identity data
UI/UX: Cleaner display with better information hierarchy and counts
Migration: More accurate comparison results and better debugging visibility
migrate-dexie-to-sqlite
Matthew Raymer 1 week ago
parent
commit
6cbd32af94
  1. 6
      package-lock.json
  2. 266
      src/services/migrationService.ts
  3. 237
      src/views/DatabaseMigration.vue

6
package-lock.json

@ -28627,9 +28627,9 @@
} }
}, },
"node_modules/terser": { "node_modules/terser": {
"version": "5.43.0", "version": "5.43.1",
"resolved": "https://registry.npmjs.org/terser/-/terser-5.43.0.tgz", "resolved": "https://registry.npmjs.org/terser/-/terser-5.43.1.tgz",
"integrity": "sha512-CqNNxKSGKSZCunSvwKLTs8u8sGGlp27sxNZ4quGh0QeNuyHM0JSEM/clM9Mf4zUp6J+tO2gUXhgXT2YMMkwfKQ==", "integrity": "sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==",
"devOptional": true, "devOptional": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {

266
src/services/migrationService.ts

@ -24,7 +24,7 @@
import { PlatformServiceFactory } from "./PlatformServiceFactory"; import { PlatformServiceFactory } from "./PlatformServiceFactory";
import { db, accountsDBPromise } from "../db/index"; import { db, accountsDBPromise } from "../db/index";
import { Contact, ContactMethod } from "../db/tables/contacts"; import { Contact, ContactMethod } from "../db/tables/contacts";
import { Settings } from "../db/tables/settings"; import { Settings, MASTER_SETTINGS_KEY } from "../db/tables/settings";
import { Account } from "../db/tables/accounts"; import { Account } from "../db/tables/accounts";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { parseJsonField } from "../db/databaseUtil"; import { parseJsonField } from "../db/databaseUtil";
@ -285,42 +285,47 @@ export async function getSqliteSettings(): Promise<Settings[]> {
return []; return [];
} }
const settings = result.values.map((row) => { const settings = result.values
const setting = parseJsonField(row, {}) as Settings; .map((row) => {
return { const setting = parseJsonField(row, {}) as Settings;
id: setting.id, return {
accountDid: setting.accountDid || "", id: setting.id,
activeDid: setting.activeDid || "", accountDid: setting.accountDid || null,
apiServer: setting.apiServer || "", activeDid: setting.activeDid || null,
filterFeedByNearby: setting.filterFeedByNearby || false, apiServer: setting.apiServer || "",
filterFeedByVisible: setting.filterFeedByVisible || false, filterFeedByNearby: setting.filterFeedByNearby || false,
finishedOnboarding: setting.finishedOnboarding || false, filterFeedByVisible: setting.filterFeedByVisible || false,
firstName: setting.firstName || "", finishedOnboarding: setting.finishedOnboarding || false,
hideRegisterPromptOnNewContact: firstName: setting.firstName || "",
setting.hideRegisterPromptOnNewContact || false, hideRegisterPromptOnNewContact:
isRegistered: setting.isRegistered || false, setting.hideRegisterPromptOnNewContact || false,
lastName: setting.lastName || "", isRegistered: setting.isRegistered || false,
lastAckedOfferToUserJwtId: setting.lastAckedOfferToUserJwtId || "", lastName: setting.lastName || "",
lastAckedOfferToUserProjectsJwtId: lastAckedOfferToUserJwtId: setting.lastAckedOfferToUserJwtId || "",
setting.lastAckedOfferToUserProjectsJwtId || "", lastAckedOfferToUserProjectsJwtId:
lastNotifiedClaimId: setting.lastNotifiedClaimId || "", setting.lastAckedOfferToUserProjectsJwtId || "",
lastViewedClaimId: setting.lastViewedClaimId || "", lastNotifiedClaimId: setting.lastNotifiedClaimId || "",
notifyingNewActivityTime: setting.notifyingNewActivityTime || "", lastViewedClaimId: setting.lastViewedClaimId || "",
notifyingReminderMessage: setting.notifyingReminderMessage || "", notifyingNewActivityTime: setting.notifyingNewActivityTime || "",
notifyingReminderTime: setting.notifyingReminderTime || "", notifyingReminderMessage: setting.notifyingReminderMessage || "",
partnerApiServer: setting.partnerApiServer || "", notifyingReminderTime: setting.notifyingReminderTime || "",
passkeyExpirationMinutes: setting.passkeyExpirationMinutes, partnerApiServer: setting.partnerApiServer || "",
profileImageUrl: setting.profileImageUrl || "", passkeyExpirationMinutes: setting.passkeyExpirationMinutes,
searchBoxes: parseJsonField(setting.searchBoxes, []), profileImageUrl: setting.profileImageUrl || "",
showContactGivesInline: setting.showContactGivesInline || false, searchBoxes: parseJsonField(setting.searchBoxes, []),
showGeneralAdvanced: setting.showGeneralAdvanced || false, showContactGivesInline: setting.showContactGivesInline || false,
showShortcutBvc: setting.showShortcutBvc || false, showGeneralAdvanced: setting.showGeneralAdvanced || false,
vapid: setting.vapid || "", showShortcutBvc: setting.showShortcutBvc || false,
warnIfProdServer: setting.warnIfProdServer || false, vapid: setting.vapid || "",
warnIfTestServer: setting.warnIfTestServer || false, warnIfProdServer: setting.warnIfProdServer || false,
webPushServer: setting.webPushServer || "", warnIfTestServer: setting.warnIfTestServer || false,
} as Settings; webPushServer: setting.webPushServer || "",
}); } as Settings;
})
.filter((setting) => {
// Only include settings that have either accountDid or activeDid set
return setting.accountDid || setting.activeDid;
});
logger.info( logger.info(
`[MigrationService] Retrieved ${settings.length} settings from SQLite`, `[MigrationService] Retrieved ${settings.length} settings from SQLite`,
@ -837,95 +842,79 @@ function accountsEqual(account1: Account, account2: Account): boolean {
*/ */
export function generateComparisonYaml(comparison: DataComparison): string { export function generateComparisonYaml(comparison: DataComparison): string {
const yaml = { const yaml = {
comparison: { summary: {
summary: { dexieContacts: comparison.dexieContacts.length,
dexieContacts: comparison.dexieContacts.length, sqliteContacts: comparison.sqliteContacts.filter(c => c.did).length,
sqliteContacts: comparison.sqliteContacts.length, dexieSettings: comparison.dexieSettings.length,
dexieSettings: comparison.dexieSettings.length, sqliteSettings: comparison.sqliteSettings.filter(s => s.accountDid || s.activeDid).length,
sqliteSettings: comparison.sqliteSettings.length, dexieAccounts: comparison.dexieAccounts.length,
dexieAccounts: comparison.dexieAccounts.length, sqliteAccounts: comparison.sqliteAccounts.filter(a => a.did).length,
sqliteAccounts: comparison.sqliteAccounts.length, },
differences: {
contacts: {
added: comparison.differences.contacts.added.length,
modified: comparison.differences.contacts.modified.length,
missing: comparison.differences.contacts.missing.filter(c => c.did).length,
}, },
differences: { settings: {
contacts: { added: comparison.differences.settings.added.length,
added: comparison.differences.contacts.added.length, modified: comparison.differences.settings.modified.length,
modified: comparison.differences.contacts.modified.length, missing: comparison.differences.settings.missing.filter(s => s.accountDid || s.activeDid).length,
missing: comparison.differences.contacts.missing.length, },
}, accounts: {
settings: { added: comparison.differences.accounts.added.length,
added: comparison.differences.settings.added.length, modified: comparison.differences.accounts.modified.length,
modified: comparison.differences.settings.modified.length, missing: comparison.differences.accounts.missing.filter(a => a.did).length,
missing: comparison.differences.settings.missing.length,
},
accounts: {
added: comparison.differences.accounts.added.length,
modified: comparison.differences.accounts.modified.length,
missing: comparison.differences.accounts.missing.length,
},
}, },
},
details: {
contacts: { contacts: {
dexie: comparison.dexieContacts.map((c) => ({ dexie: comparison.dexieContacts.map((c) => ({
did: c.did, did: c.did,
name: c.name, name: c.name || '<empty>',
notes: c.notes, contactMethods: (c.contactMethods || []).length,
profileImageUrl: c.profileImageUrl,
seesMe: c.seesMe,
registered: c.registered,
contactMethods: c.contactMethods,
})),
sqlite: comparison.sqliteContacts.map((c) => ({
did: c.did,
name: c.name,
notes: c.notes,
profileImageUrl: c.profileImageUrl,
seesMe: c.seesMe,
registered: c.registered,
contactMethods: c.contactMethods,
})), })),
sqlite: comparison.sqliteContacts
.filter(c => c.did)
.map((c) => ({
did: c.did,
name: c.name || '<empty>',
contactMethods: (c.contactMethods || []).length,
})),
}, },
settings: { settings: {
dexie: comparison.dexieSettings.map((s) => ({ dexie: comparison.dexieSettings.map((s) => ({
id: s.id, id: s.id,
accountDid: s.accountDid, type: s.id === MASTER_SETTINGS_KEY ? 'master' : 'account',
activeDid: s.activeDid, did: s.activeDid || s.accountDid,
firstName: s.firstName, isRegistered: s.isRegistered || false,
isRegistered: s.isRegistered,
profileImageUrl: s.profileImageUrl,
showShortcutBvc: s.showShortcutBvc,
searchBoxes: s.searchBoxes,
})),
sqlite: comparison.sqliteSettings.map((s) => ({
id: s.id,
accountDid: s.accountDid,
activeDid: s.activeDid,
firstName: s.firstName,
isRegistered: s.isRegistered,
profileImageUrl: s.profileImageUrl,
showShortcutBvc: s.showShortcutBvc,
searchBoxes: s.searchBoxes,
})), })),
sqlite: comparison.sqliteSettings
.filter(s => s.accountDid || s.activeDid)
.map((s) => ({
id: s.id,
type: s.id === MASTER_SETTINGS_KEY ? 'master' : 'account',
did: s.activeDid || s.accountDid,
isRegistered: s.isRegistered || false,
})),
}, },
accounts: { accounts: {
dexie: comparison.dexieAccounts.map((a) => ({ dexie: comparison.dexieAccounts.map((a) => ({
id: a.id, id: a.id,
dateCreated: a.dateCreated,
derivationPath: a.derivationPath,
did: a.did, did: a.did,
identity: a.identity,
mnemonic: a.mnemonic,
passkeyCredIdHex: a.passkeyCredIdHex,
publicKeyHex: a.publicKeyHex,
})),
sqlite: comparison.sqliteAccounts.map((a) => ({
id: a.id,
dateCreated: a.dateCreated, dateCreated: a.dateCreated,
derivationPath: a.derivationPath, hasIdentity: !!a.identity,
did: a.did, hasMnemonic: !!a.mnemonic,
identity: a.identity,
mnemonic: a.mnemonic,
passkeyCredIdHex: a.passkeyCredIdHex,
publicKeyHex: a.publicKeyHex,
})), })),
sqlite: comparison.sqliteAccounts
.filter(a => a.did)
.map((a) => ({
id: a.id,
did: a.did,
dateCreated: a.dateCreated,
hasIdentity: !!a.identity,
hasMnemonic: !!a.mnemonic,
})),
}, },
}, },
}; };
@ -1111,29 +1100,45 @@ export async function migrateSettings(
[dexieSetting.id], [dexieSetting.id],
); );
// Prepare the data object, handling DIDs based on whether this is the master settings
const settingData: Record<string, unknown> = {};
fieldsToMigrate.forEach((field) => {
if (dexieSetting[field as keyof Settings] !== undefined) {
settingData[field] = dexieSetting[field as keyof Settings];
}
});
// Handle DIDs based on whether this is the master settings
if (dexieSetting.id === MASTER_SETTINGS_KEY) {
// Master settings should only use activeDid
if (dexieSetting.activeDid) {
settingData.activeDid = dexieSetting.activeDid;
}
// Ensure accountDid is null for master settings
settingData.accountDid = null;
} else {
// Non-master settings should only use accountDid
if (dexieSetting.accountDid) {
settingData.accountDid = dexieSetting.accountDid;
}
// Ensure activeDid is null for non-master settings
settingData.activeDid = null;
}
if (existingResult?.values?.length) { if (existingResult?.values?.length) {
if (overwriteExisting) { if (overwriteExisting) {
// Update existing setting with only the specified fields // Update existing setting
const updateData: Record<string, unknown> = {}; const { sql, params } = generateUpdateStatement(
fieldsToMigrate.forEach((field) => { settingData,
if (dexieSetting[field as keyof Settings] !== undefined) { "settings",
updateData[field] = dexieSetting[field as keyof Settings]; "id = ?",
} [dexieSetting.id],
}); );
await platformService.dbExec(sql, params);
if (Object.keys(updateData).length > 0) { result.settingsMigrated++;
const { sql, params } = generateUpdateStatement( logger.info(
updateData as unknown as Record<string, unknown>, `[MigrationService] Updated settings: ${dexieSetting.id}`,
"settings", );
"id = ?",
[dexieSetting.id],
);
await platformService.dbExec(sql, params);
result.settingsMigrated++;
logger.info(
`[MigrationService] Updated settings: ${dexieSetting.id}`,
);
}
} else { } else {
result.warnings.push( result.warnings.push(
`Settings ${dexieSetting.id} already exists, skipping`, `Settings ${dexieSetting.id} already exists, skipping`,
@ -1141,8 +1146,9 @@ export async function migrateSettings(
} }
} else { } else {
// Insert new setting // Insert new setting
settingData.id = dexieSetting.id;
const { sql, params } = generateInsertStatement( const { sql, params } = generateInsertStatement(
dexieSetting as unknown as Record<string, unknown>, settingData,
"settings", "settings",
); );
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);

237
src/views/DatabaseMigration.vue

@ -440,17 +440,59 @@
class="mt-4" class="mt-4"
> >
<h4 class="text-sm font-medium text-gray-900 mb-2"> <h4 class="text-sm font-medium text-gray-900 mb-2">
Added Contacts: Added Contacts ({{ comparison.differences.contacts.added.length }}):
</h4> </h4>
<div class="max-h-32 overflow-y-auto space-y-1"> <div class="space-y-1">
<div <div
v-for="contact in comparison.differences.contacts.added" v-for="contact in comparison.differences.contacts.added"
:key="contact.did" :key="contact.did"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded" class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
> >
{{ contact.name || "Unnamed" }} ({{ <div class="font-medium">{{ contact.name || '<empty>' }}</div>
contact.did.substring(0, 20) <div class="text-gray-500">{{ contact.did }}</div>
}}...) <div class="text-gray-400">{{ contact.contactMethods?.length || 0 }} contact methods</div>
</div>
</div>
</div>
<!-- Modified Contacts -->
<div
v-if="comparison.differences.contacts.modified.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Modified 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>
<!-- Missing 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">
Missing 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>
@ -521,15 +563,59 @@
class="mt-4" class="mt-4"
> >
<h4 class="text-sm font-medium text-gray-900 mb-2"> <h4 class="text-sm font-medium text-gray-900 mb-2">
Added Settings: Added Settings ({{ comparison.differences.settings.added.length }}):
</h4> </h4>
<div class="max-h-32 overflow-y-auto space-y-1"> <div class="space-y-1">
<div <div
v-for="setting in comparison.differences.settings.added" v-for="setting in comparison.differences.settings.added"
:key="setting.id" :key="setting.id"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded" class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
> >
ID: {{ setting.id }} - {{ setting.firstName || "Unnamed" }} <div class="font-medium">ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})</div>
<div class="text-gray-500">{{ setting.activeDid || setting.accountDid }}</div>
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
</div>
</div>
</div>
<!-- Modified Settings -->
<div
v-if="comparison.differences.settings.modified.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Modified 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">ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})</div>
<div class="text-gray-500">{{ setting.activeDid || setting.accountDid }}</div>
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
</div>
</div>
</div>
<!-- Missing 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">
Missing 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">ID: {{ setting.id }} ({{ setting.id === 1 ? 'master' : 'account' }})</div>
<div class="text-gray-500">{{ setting.activeDid || setting.accountDid }}</div>
<div class="text-gray-400">Registered: {{ setting.isRegistered ? 'Yes' : 'No' }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -600,15 +686,65 @@
class="mt-4" class="mt-4"
> >
<h4 class="text-sm font-medium text-gray-900 mb-2"> <h4 class="text-sm font-medium text-gray-900 mb-2">
Added Accounts: Added Accounts ({{ comparison.differences.accounts.added.length }}):
</h4> </h4>
<div class="max-h-32 overflow-y-auto space-y-1"> <div class="space-y-1">
<div <div
v-for="account in comparison.differences.accounts.added" v-for="account in comparison.differences.accounts.added"
:key="account.id" :key="account.id"
class="text-xs text-gray-600 bg-gray-50 p-2 rounded" class="text-xs text-gray-600 bg-gray-50 p-2 rounded"
> >
ID: {{ account.id }} - {{ account.did.substring(0, 20) }}... <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: {{ account.identity ? 'Yes' : 'No' }}</div>
<div class="text-gray-400">Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}</div>
</div>
</div>
</div>
<!-- Modified Accounts -->
<div
v-if="comparison.differences.accounts.modified.length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Modified Accounts ({{ comparison.differences.accounts.modified.length }}):
</h4>
<div class="space-y-1">
<div
v-for="account in comparison.differences.accounts.modified"
: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: {{ account.identity ? 'Yes' : 'No' }}</div>
<div class="text-gray-400">Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}</div>
</div>
</div>
</div>
<!-- Missing Accounts -->
<div
v-if="comparison.differences.accounts.missing.filter(a => a.did).length > 0"
class="mt-4"
>
<h4 class="text-sm font-medium text-gray-900 mb-2">
Missing Accounts ({{ comparison.differences.accounts.missing.filter(a => a.did).length }}):
</h4>
<div class="space-y-1">
<div
v-for="account in comparison.differences.accounts.missing.filter(a => a.did)"
: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: {{ account.identity ? 'Yes' : 'No' }}</div>
<div class="text-gray-400">Has Mnemonic: {{ account.mnemonic ? 'Yes' : 'No' }}</div>
</div> </div>
</div> </div>
</div> </div>
@ -746,6 +882,9 @@ export default class DatabaseMigration extends Vue {
"[DatabaseMigration] Complete migration successful", "[DatabaseMigration] Complete migration successful",
result, result,
); );
// Refresh comparison data after successful migration
this.comparison = await compareDatabases();
} else { } else {
this.error = `Migration failed: ${result.errors.join(", ")}`; this.error = `Migration failed: ${result.errors.join(", ")}`;
logger.error( logger.error(
@ -819,6 +958,9 @@ export default class DatabaseMigration extends Vue {
"[DatabaseMigration] Contact migration completed successfully", "[DatabaseMigration] Contact migration completed successfully",
result, result,
); );
// Refresh comparison data after successful migration
this.comparison = await compareDatabases();
} else { } else {
this.error = `Migration failed: ${result.errors.join(", ")}`; this.error = `Migration failed: ${result.errors.join(", ")}`;
logger.error( logger.error(
@ -861,6 +1003,9 @@ export default class DatabaseMigration extends Vue {
"[DatabaseMigration] Settings migration completed successfully", "[DatabaseMigration] Settings migration completed successfully",
result, result,
); );
// Refresh comparison data after successful migration
this.comparison = await compareDatabases();
} else { } else {
this.error = `Migration failed: ${result.errors.join(", ")}`; this.error = `Migration failed: ${result.errors.join(", ")}`;
logger.error( logger.error(
@ -904,6 +1049,9 @@ export default class DatabaseMigration extends Vue {
"[DatabaseMigration] Account migration completed successfully", "[DatabaseMigration] Account migration completed successfully",
result, result,
); );
// Refresh comparison data after successful migration
this.comparison = await compareDatabases();
} else { } else {
this.error = `Migration failed: ${result.errors.join(", ")}`; this.error = `Migration failed: ${result.errors.join(", ")}`;
logger.error( logger.error(
@ -937,32 +1085,69 @@ export default class DatabaseMigration extends Vue {
try { try {
const newComparison = await compareDatabases(); const newComparison = await compareDatabases();
// Check if there are any remaining differences // Calculate differences by type for each table
const totalRemaining = const differences = {
newComparison.differences.contacts.added.length + contacts: {
newComparison.differences.contacts.modified.length + added: newComparison.differences.contacts.added.length,
newComparison.differences.contacts.missing.length + modified: newComparison.differences.contacts.modified.length,
newComparison.differences.settings.added.length + missing: newComparison.differences.contacts.missing.length,
newComparison.differences.settings.modified.length + },
newComparison.differences.settings.missing.length + settings: {
newComparison.differences.accounts.added.length + added: newComparison.differences.settings.added.length,
newComparison.differences.accounts.modified.length + modified: newComparison.differences.settings.modified.length,
newComparison.differences.accounts.missing.length; missing: newComparison.differences.settings.missing.length,
},
accounts: {
added: newComparison.differences.accounts.added.length,
modified: newComparison.differences.accounts.modified.length,
missing: newComparison.differences.accounts.missing.length,
},
};
const totalRemaining = Object.values(differences).reduce(
(sum, table) =>
sum + table.added + table.modified + table.missing,
0
);
// Build a detailed message
const detailMessages = [];
if (differences.contacts.added + differences.contacts.modified + differences.contacts.missing > 0) {
detailMessages.push(
`Contacts: ${differences.contacts.added} to add, ${differences.contacts.modified} modified, ${differences.contacts.missing} missing`
);
}
if (differences.settings.added + differences.settings.modified + differences.settings.missing > 0) {
detailMessages.push(
`Settings: ${differences.settings.added} to add, ${differences.settings.modified} modified, ${differences.settings.missing} missing`
);
}
if (differences.accounts.added + differences.accounts.modified + differences.accounts.missing > 0) {
detailMessages.push(
`Accounts: ${differences.accounts.added} to add, ${differences.accounts.modified} modified, ${differences.accounts.missing} missing`
);
}
if (totalRemaining === 0) { if (totalRemaining === 0) {
this.successMessage = this.successMessage =
"✅ Migration verification successful! All data has been migrated correctly."; "✅ Migration verification successful! All data has been migrated correctly.";
logger.info( logger.info(
"[DatabaseMigration] Migration verification successful - no differences found", "[DatabaseMigration] Migration verification successful - no differences found"
); );
} else { } else {
this.successMessage = `⚠️ Migration verification completed. Found ${totalRemaining} remaining differences. Consider running additional migrations if needed.`; this.successMessage = `⚠️ Migration verification completed. Found ${totalRemaining} remaining differences:\n${detailMessages.join("\n")}`;
if (differences.settings.modified > 0 || differences.settings.missing > 0) {
this.successMessage += "\n\nNote: Some settings differences may be expected due to default values in SQLite.";
}
logger.warn( logger.warn(
"[DatabaseMigration] Migration verification found remaining differences", "[DatabaseMigration] Migration verification found remaining differences",
{ {
remaining: totalRemaining, remaining: totalRemaining,
differences: newComparison.differences, differences: differences,
}, }
); );
} }

Loading…
Cancel
Save