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.
 
 
 

918 lines
32 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 class="mb-8">
<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>
</div>
<!-- Status Banner -->
<div
v-if="!isDexieEnabled"
class="mb-6 bg-yellow-50 border border-yellow-200 rounded-lg p-4"
>
<div class="flex">
<div class="flex-shrink-0">
<IconRenderer
icon-name="warning"
svg-class="h-5 w-5 text-yellow-400"
fill="currentColor"
/>
</div>
<div class="ml-3">
<h3 class="text-sm font-medium text-yellow-800">
Dexie Database Disabled
</h3>
<div class="mt-2 text-sm text-yellow-700">
<p>
To use migration features, enable Dexie database by setting
<code class="bg-yellow-100 px-1 rounded">
USE_DEXIE_DB = true
</code>
in
<code class="bg-yellow-100 px-1 rounded">
constants/app.ts
</code>
</p>
</div>
</div>
</div>
</div>
<!-- Action Buttons -->
<div class="mb-8 flex flex-wrap gap-4">
<button
:disabled="isLoading || !isDexieEnabled"
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 || !isDexieEnabled || !comparison"
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>
<button
:disabled="isLoading || !isDexieEnabled || !comparison"
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 || !isDexieEnabled || !comparison"
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="!comparison"
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-teal-600 hover:bg-teal-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-teal-500 disabled:opacity-50 disabled:cursor-not-allowed"
@click="verifyMigration"
>
<IconRenderer icon-name="check" svg-class="-ml-1 mr-3 h-5 w-5" />
Verify Migration
</button>
</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>
<!-- 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>
<!-- 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">
<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 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>
<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>
</div>
<!-- Differences Section -->
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- Contacts Differences -->
<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">Added</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"
>Modified</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-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"
>Missing</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">
Added Contacts:
</h4>
<div class="max-h-32 overflow-y-auto 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"
>
{{ contact.name || "Unnamed" }} ({{
contact.did.substring(0, 20)
}}...)
</div>
</div>
</div>
</div>
</div>
<!-- Settings Differences -->
<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">Added</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"
>Modified</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-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"
>Missing</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">
Added Settings:
</h4>
<div class="max-h-32 overflow-y-auto 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"
>
ID: {{ setting.id }} - {{ setting.firstName || "Unnamed" }}
</div>
</div>
</div>
</div>
</div>
<!-- Account Differences -->
<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">Added</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="edit"
svg-class="h-5 w-5 text-yellow-600 mr-2"
/>
<span class="text-sm font-medium text-yellow-900"
>Modified</span
>
</div>
<span class="text-sm font-bold text-yellow-900">{{
comparison.differences.accounts.modified.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"
>Missing</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">
Added Accounts:
</h4>
<div class="max-h-32 overflow-y-auto 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"
>
ID: {{ account.id }} - {{ account.did.substring(0, 20) }}...
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Migration Options -->
<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>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-facing-decorator";
import IconRenderer from "../components/IconRenderer.vue";
import {
compareDatabases,
migrateContacts,
migrateSettings,
migrateAccounts,
generateComparisonYaml,
type DataComparison,
type MigrationResult,
} from "../services/migrationService";
import { USE_DEXIE_DB } from "../constants/app";
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 {
// Component state
private isLoading = false;
private loadingMessage = "";
private error = "";
private successMessage = "";
private comparison: DataComparison | null = null;
private overwriteExisting = false;
/**
* Computed property to check if Dexie database is enabled
*
* @returns {boolean} True if Dexie database is enabled, false otherwise
*/
get isDexieEnabled(): boolean {
return USE_DEXIE_DB;
}
/**
* 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,
* with options to overwrite existing records.
*
* @async
* @returns {Promise<void>}
*/
async migrateContacts(): Promise<void> {
this.setLoading("Migrating contacts...");
this.clearMessages();
try {
const result: MigrationResult = await migrateContacts(
this.overwriteExisting,
);
if (result.success) {
this.successMessage = `Successfully migrated ${result.contactsMigrated} contacts.`;
if (result.warnings.length > 0) {
this.successMessage += ` ${result.warnings.length} warnings.`;
}
logger.info(
"[DatabaseMigration] Contact migration completed successfully",
result,
);
} else {
this.error = `Migration failed: ${result.errors.join(", ")}`;
logger.error(
"[DatabaseMigration] Contact migration failed:",
result.errors,
);
}
} catch (error) {
this.error = `Failed to migrate contacts: ${error}`;
logger.error("[DatabaseMigration] Contact migration failed:", error);
} finally {
this.setLoading("");
}
}
/**
* Migrates settings from Dexie to SQLite database
*
* This method transfers settings from the Dexie database to SQLite,
* with options to overwrite existing records.
*
* @async
* @returns {Promise<void>}
*/
async migrateSettings(): Promise<void> {
this.setLoading("Migrating settings...");
this.clearMessages();
try {
const result: MigrationResult = await migrateSettings(
this.overwriteExisting,
);
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,
);
} 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,
* with options to overwrite existing records. 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(
this.overwriteExisting,
);
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,
);
} 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("");
}
}
/**
* Verifies the migration by running another comparison
*
* This method runs a fresh comparison between Dexie and SQLite databases
* to verify that the migration was successful. It's useful to run this
* after completing migrations to ensure data integrity and relationship
* preservation.
*
* @async
* @returns {Promise<void>}
*/
async verifyMigration(): Promise<void> {
this.setLoading("Verifying migration...");
this.clearMessages();
try {
const newComparison = await compareDatabases();
// Check if there are any remaining differences
const totalRemaining =
newComparison.differences.contacts.added.length +
newComparison.differences.contacts.modified.length +
newComparison.differences.contacts.missing.length +
newComparison.differences.settings.added.length +
newComparison.differences.settings.modified.length +
newComparison.differences.settings.missing.length +
newComparison.differences.accounts.added.length +
newComparison.differences.accounts.modified.length +
newComparison.differences.accounts.missing.length;
if (totalRemaining === 0) {
this.successMessage =
"✅ Migration verification successful! All data has been migrated correctly.";
logger.info(
"[DatabaseMigration] Migration verification successful - no differences found",
);
} else {
this.successMessage = `⚠️ Migration verification completed. Found ${totalRemaining} remaining differences. Consider running additional migrations if needed.`;
logger.warn(
"[DatabaseMigration] Migration verification found remaining differences",
{
remaining: totalRemaining,
differences: newComparison.differences,
},
);
}
// Update the comparison to show the current state
this.comparison = newComparison;
} catch (error) {
this.error = `Failed to verify migration: ${error}`;
logger.error("[DatabaseMigration] Migration verification failed:", error);
} finally {
this.setLoading("");
}
}
/**
* Exports comparison results to a file
*
* This method generates a YAML-formatted comparison and triggers
* a file download for the user.
*
* @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 link = document.createElement("a");
link.href = url;
link.download = `database-comparison-${new Date().toISOString().split("T")[0]}.json`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(url);
this.successMessage = "Comparison exported successfully";
logger.info("[DatabaseMigration] Comparison exported successfully");
} catch (error) {
this.error = `Failed to export comparison: ${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>