Browse Source

feat(export): Replace CSV export with standardized JSON format

- Add contactsToExportJson utility function for standardized data export
- Replace CSV export with JSON format in DataExportSection
- Update file extension and MIME type to application/json
- Remove Dexie-specific export logic in favor of unified SQLite/Dexie approach
- Update success notifications to reflect JSON format
- Add TypeScript interfaces for export data structure

This change improves data portability and standardization by:
- Using a consistent JSON format for data export/import
- Supporting both SQLite and Dexie databases
- Including all contact fields in export
- Properly handling contactMethods as stringified JSON
- Maintaining backward compatibility with existing import tools

Security: No sensitive data exposure, maintains existing access controls
pull/137/head
Matthew Raymer 2 weeks ago
parent
commit
3d8e40e92b
  1. 51
      src/components/DataExportSection.vue
  2. 36
      src/db/databaseUtil.ts
  3. 109
      src/libs/util.ts
  4. 20
      src/services/migrationService.ts
  5. 12
      src/services/platforms/CapacitorPlatformService.ts
  6. 3
      src/views/AccountViewView.vue
  7. 114
      src/views/HomeView.vue
  8. 4
      src/views/IdentitySwitcherView.vue
  9. 7
      src/views/ProjectViewView.vue

51
src/components/DataExportSection.vue

@ -63,13 +63,18 @@ backup and database export, with platform-specific download instructions. * *
<script lang="ts"> <script lang="ts">
import { Component, Prop, Vue } from "vue-facing-decorator"; import { Component, Prop, Vue } from "vue-facing-decorator";
import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app"; import { AppString, NotificationIface, USE_DEXIE_DB } from "../constants/app";
import { db } from "../db/index";
import { Contact } from "../db/tables/contacts";
import * as databaseUtil from "../db/databaseUtil";
import { logger } from "../utils/logger"; import { logger } from "../utils/logger";
import { PlatformServiceFactory } from "../services/PlatformServiceFactory"; import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
import { import {
PlatformService, PlatformService,
PlatformCapabilities, PlatformCapabilities,
} from "../services/PlatformService"; } from "../services/PlatformService";
import { contactsToExportJson } from "../libs/util";
import { db } from "../db/index";
/** /**
* @vue-component * @vue-component
@ -131,24 +136,25 @@ export default class DataExportSection extends Vue {
*/ */
public async exportDatabase() { public async exportDatabase() {
try { try {
if (!USE_DEXIE_DB) { let allContacts: Contact[] = [];
throw new Error("Not implemented"); const platformService = PlatformServiceFactory.getInstance();
const result = await platformService.dbQuery(`SELECT * FROM contacts`);
if (result) {
allContacts = databaseUtil.mapQueryResultToValues(
result,
) as unknown as Contact[];
} }
const blob = await db.export({ // if (USE_DEXIE_DB) {
prettyJson: true, // await db.open();
transform: (table, value, key) => { // allContacts = await db.contacts.toArray();
if (table === "contacts") { // }
// Dexie inserts a number 0 when some are undefined, so we need to totally remove them.
Object.keys(value).forEach((prop) => { // Convert contacts to export format
if (value[prop] === undefined) { const exportData = contactsToExportJson(allContacts);
delete value[prop]; const jsonStr = JSON.stringify(exportData, null, 2);
} const blob = new Blob([jsonStr], { type: "application/json" });
});
} const fileName = `${AppString.APP_NAME_NO_SPACES}-backup-contacts.json`;
return { value, key };
},
});
const fileName = `${AppString.APP_NAME_NO_SPACES}-backup.json`;
if (this.platformCapabilities.hasFileDownload) { if (this.platformCapabilities.hasFileDownload) {
// Web platform: Use download link // Web platform: Use download link
@ -160,8 +166,7 @@ export default class DataExportSection extends Vue {
setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000); setTimeout(() => URL.revokeObjectURL(this.downloadUrl), 1000);
} else if (this.platformCapabilities.hasFileSystem) { } else if (this.platformCapabilities.hasFileSystem) {
// Native platform: Write to app directory // Native platform: Write to app directory
const content = await blob.text(); await this.platformService.writeAndShareFile(fileName, jsonStr);
await this.platformService.writeAndShareFile(fileName, content);
} else { } else {
throw new Error("This platform does not support file downloads."); throw new Error("This platform does not support file downloads.");
} }
@ -172,10 +177,10 @@ export default class DataExportSection extends Vue {
type: "success", type: "success",
title: "Export Successful", title: "Export Successful",
text: this.platformCapabilities.hasFileDownload text: this.platformCapabilities.hasFileDownload
? "See your downloads directory for the backup. It is in the Dexie format." ? "See your downloads directory for the backup. It is in JSON format."
: "You should have been prompted to save your backup file.", : "The backup file has been saved.",
}, },
-1, 3000,
); );
} catch (error) { } catch (error) {
logger.error("Export Error:", error); logger.error("Export Error:", error);

36
src/db/databaseUtil.ts

@ -79,13 +79,9 @@ const DEFAULT_SETTINGS: Settings = {
// retrieves default settings // retrieves default settings
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> { export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
console.log("[databaseUtil] retrieveSettingsForDefaultAccount");
const platform = PlatformServiceFactory.getInstance(); const platform = PlatformServiceFactory.getInstance();
const sql = "SELECT * FROM settings WHERE id = ?"; const sql = "SELECT * FROM settings WHERE id = ?";
console.log("[databaseUtil] sql", sql);
const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]); const result = await platform.dbQuery(sql, [MASTER_SETTINGS_KEY]);
console.log("[databaseUtil] result", JSON.stringify(result, null, 2));
console.trace("Trace from [retrieveSettingsForDefaultAccount]");
if (!result) { if (!result) {
return DEFAULT_SETTINGS; return DEFAULT_SETTINGS;
} else { } else {
@ -103,21 +99,21 @@ export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
/** /**
* Retrieves settings for the active account, merging with default settings * Retrieves settings for the active account, merging with default settings
* *
* @returns Promise<Settings> Combined settings with account-specific overrides * @returns Promise<Settings> Combined settings with account-specific overrides
* @throws Will log specific errors for debugging but returns default settings on failure * @throws Will log specific errors for debugging but returns default settings on failure
*/ */
export async function retrieveSettingsForActiveAccount(): Promise<Settings> { export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
logConsoleAndDb("[databaseUtil] Starting settings retrieval for active account");
try { try {
// Get default settings first // Get default settings first
const defaultSettings = await retrieveSettingsForDefaultAccount(); const defaultSettings = await retrieveSettingsForDefaultAccount();
logConsoleAndDb(`[databaseUtil] Retrieved default settings (hasActiveDid: ${!!defaultSettings.activeDid})`);
// If no active DID, return defaults // If no active DID, return defaults
if (!defaultSettings.activeDid) { if (!defaultSettings.activeDid) {
logConsoleAndDb("[databaseUtil] No active DID found, returning default settings"); logConsoleAndDb(
"[databaseUtil] No active DID found, returning default settings",
);
return defaultSettings; return defaultSettings;
} }
@ -130,12 +126,17 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
); );
if (!result?.values?.length) { if (!result?.values?.length) {
logConsoleAndDb(`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`); logConsoleAndDb(
`[databaseUtil] No account-specific settings found for ${defaultSettings.activeDid}`,
);
return defaultSettings; return defaultSettings;
} }
// Map and filter settings // Map and filter settings
const overrideSettings = mapColumnsToValues(result.columns, result.values)[0] as Settings; const overrideSettings = mapColumnsToValues(
result.columns,
result.values,
)[0] as Settings;
const overrideSettingsFiltered = Object.fromEntries( const overrideSettingsFiltered = Object.fromEntries(
Object.entries(overrideSettings).filter(([_, v]) => v !== null), Object.entries(overrideSettings).filter(([_, v]) => v !== null),
); );
@ -151,30 +152,27 @@ export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
} catch (error) { } catch (error) {
logConsoleAndDb( logConsoleAndDb(
`[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`, `[databaseUtil] Failed to parse searchBoxes for ${defaultSettings.activeDid}: ${error}`,
true true,
); );
// Reset to empty array on parse failure // Reset to empty array on parse failure
settings.searchBoxes = []; settings.searchBoxes = [];
} }
} }
logConsoleAndDb(
`[databaseUtil] Successfully merged settings for ${defaultSettings.activeDid} ` +
`(overrides: ${Object.keys(overrideSettingsFiltered).length})`
);
return settings; return settings;
} catch (error) { } catch (error) {
logConsoleAndDb( logConsoleAndDb(
`[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`, `[databaseUtil] Failed to retrieve account settings for ${defaultSettings.activeDid}: ${error}`,
true true,
); );
// Return defaults on error // Return defaults on error
return defaultSettings; return defaultSettings;
} }
} catch (error) { } catch (error) {
logConsoleAndDb(`[databaseUtil] Failed to retrieve default settings: ${error}`, true); logConsoleAndDb(
`[databaseUtil] Failed to retrieve default settings: ${error}`,
true,
);
// Return minimal default settings on complete failure // Return minimal default settings on complete failure
return { return {
id: MASTER_SETTINGS_KEY, id: MASTER_SETTINGS_KEY,

109
src/libs/util.ts

@ -80,7 +80,10 @@ export const UNIT_LONG: Record<string, string> = {
}; };
/* eslint-enable prettier/prettier */ /* eslint-enable prettier/prettier */
const UNIT_CODES: Record<string, { name: string; faIcon: string; decimals: number }> = { const UNIT_CODES: Record<
string,
{ name: string; faIcon: string; decimals: number }
> = {
BTC: { BTC: {
name: "Bitcoin", name: "Bitcoin",
faIcon: "bitcoin-sign", faIcon: "bitcoin-sign",
@ -558,20 +561,15 @@ export const retrieveAccountMetadata = async (
}; };
export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => { export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
console.log("[retrieveAllAccountsMetadata] start");
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
const sql = `SELECT * FROM accounts`; const sql = `SELECT * FROM accounts`;
console.log("[retrieveAllAccountsMetadata] sql: ", sql);
const dbAccounts = await platformService.dbQuery(sql); const dbAccounts = await platformService.dbQuery(sql);
console.log("[retrieveAllAccountsMetadata] dbAccounts: ", dbAccounts);
const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[]; const accounts = databaseUtil.mapQueryResultToValues(dbAccounts) as Account[];
let result = accounts.map((account) => { let result = accounts.map((account) => {
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
const { identity, mnemonic, ...metadata } = account; const { identity, mnemonic, ...metadata } = account;
return metadata as Account; return metadata as Account;
}); });
console.log("[retrieveAllAccountsMetadata] result: ", result);
console.log("[retrieveAllAccountsMetadata] USE_DEXIE_DB: ", USE_DEXIE_DB);
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
// one of the few times we use accountsDBPromise directly; try to avoid more usage // one of the few times we use accountsDBPromise directly; try to avoid more usage
const accountsDB = await accountsDBPromise; const accountsDB = await accountsDBPromise;
@ -582,7 +580,6 @@ export const retrieveAllAccountsMetadata = async (): Promise<Account[]> => {
return metadata as Account; return metadata as Account;
}); });
} }
console.log("[retrieveAllAccountsMetadata] end", JSON.stringify(result, null, 2));
return result; return result;
}; };
@ -663,10 +660,6 @@ export async function saveNewIdentity(
derivationPath: string, derivationPath: string,
): Promise<void> { ): Promise<void> {
try { try {
console.log("[saveNewIdentity] identity", identity);
console.log("[saveNewIdentity] mnemonic", mnemonic);
console.log("[saveNewIdentity] newId", newId);
console.log("[saveNewIdentity] derivationPath", derivationPath);
// add to the new sql db // add to the new sql db
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
const secrets = await platformService.dbQuery( const secrets = await platformService.dbQuery(
@ -685,7 +678,6 @@ export async function saveNewIdentity(
const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic); const encryptedMnemonicBase64 = arrayBufferToBase64(encryptedMnemonic);
const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex) const sql = `INSERT INTO accounts (dateCreated, derivationPath, did, identityEncrBase64, mnemonicEncrBase64, publicKeyHex)
VALUES (?, ?, ?, ?, ?, ?)`; VALUES (?, ?, ?, ?, ?, ?)`;
console.log("[saveNewIdentity] sql: ", sql);
const params = [ const params = [
new Date().toISOString(), new Date().toISOString(),
derivationPath, derivationPath,
@ -694,7 +686,6 @@ export async function saveNewIdentity(
encryptedMnemonicBase64, encryptedMnemonicBase64,
newId.keys[0].publicKeyHex, newId.keys[0].publicKeyHex,
]; ];
console.log("[saveNewIdentity] params: ", params);
await platformService.dbExec(sql, params); await platformService.dbExec(sql, params);
await databaseUtil.updateDefaultSettings({ activeDid: newId.did }); await databaseUtil.updateDefaultSettings({ activeDid: newId.did });
@ -712,7 +703,6 @@ export async function saveNewIdentity(
await updateDefaultSettings({ activeDid: newId.did }); await updateDefaultSettings({ activeDid: newId.did });
} }
} catch (error) { } catch (error) {
console.log("[saveNewIdentity] error: ", error);
logger.error("Failed to update default settings:", error); logger.error("Failed to update default settings:", error);
throw new Error( throw new Error(
"Failed to set default settings. Please try again or restart the app.", "Failed to set default settings. Please try again or restart the app.",
@ -837,3 +827,94 @@ export const sendTestThroughPushServer = async (
logger.log("Got response from web push server:", response); logger.log("Got response from web push server:", response);
return response; return response;
}; };
/**
* Converts a Contact object to a CSV line string following the established format.
* The format matches CONTACT_CSV_HEADER: "name,did,pubKeyBase64,seesMe,registered,contactMethods"
* where contactMethods is stored as a stringified JSON array.
*
* @param contact - The Contact object to convert
* @returns A CSV-formatted string representing the contact
* @throws {Error} If the contact object is missing required fields
*/
export const contactToCsvLine = (contact: Contact): string => {
if (!contact.did) {
throw new Error("Contact must have a did field");
}
// Escape fields that might contain commas or quotes
const escapeField = (field: string | boolean | undefined): string => {
if (field === undefined) return "";
const str = String(field);
if (str.includes(",") || str.includes('"')) {
return `"${str.replace(/"/g, '""')}"`;
}
return str;
};
// Handle contactMethods array by stringifying it
const contactMethodsStr = contact.contactMethods
? escapeField(JSON.stringify(contact.contactMethods))
: "";
const fields = [
escapeField(contact.name),
escapeField(contact.did),
escapeField(contact.publicKeyBase64),
escapeField(contact.seesMe),
escapeField(contact.registered),
contactMethodsStr,
];
return fields.join(",");
};
/**
* Interface for the JSON export format of database tables
*/
export interface TableExportData {
tableName: string;
inbound: boolean;
rows: Array<Record<string, unknown>>;
}
/**
* Interface for the complete database export format
*/
export interface DatabaseExport {
data: {
data: Array<TableExportData>;
};
}
/**
* Converts an array of contacts to the standardized database export JSON format.
* This format is used for data migration and backup purposes.
*
* @param contacts - Array of Contact objects to convert
* @returns DatabaseExport object in the standardized format
*/
export const contactsToExportJson = (contacts: Contact[]): DatabaseExport => {
// Convert each contact to a plain object and ensure all fields are included
const rows = contacts.map(contact => ({
did: contact.did,
name: contact.name || null,
contactMethods: contact.contactMethods ? JSON.stringify(contact.contactMethods) : null,
nextPubKeyHashB64: contact.nextPubKeyHashB64 || null,
notes: contact.notes || null,
profileImageUrl: contact.profileImageUrl || null,
publicKeyBase64: contact.publicKeyBase64 || null,
seesMe: contact.seesMe || false,
registered: contact.registered || false
}));
return {
data: {
data: [{
tableName: "contacts",
inbound: true,
rows
}]
}
};
};

20
src/services/migrationService.ts

@ -31,8 +31,6 @@ export class MigrationService {
sqlQuery: (sql: string) => Promise<T>, sqlQuery: (sql: string) => Promise<T>,
extractMigrationNames: (result: T) => Set<string>, extractMigrationNames: (result: T) => Set<string>,
): Promise<void> { ): Promise<void> {
// eslint-disable-next-line no-console
console.log("Will run migrations");
// Create migrations table if it doesn't exist // Create migrations table if it doesn't exist
const result0 = await sqlExec(` const result0 = await sqlExec(`
@ -42,29 +40,21 @@ export class MigrationService {
executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP executed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
); );
`); `);
// eslint-disable-next-line no-console
console.log("Created migrations table", JSON.stringify(result0));
// Get list of executed migrations // Get list of executed migrations
const result1: T = await sqlQuery("SELECT name FROM migrations;"); const result1: T = await sqlQuery("SELECT name FROM migrations;");
const executedMigrations = extractMigrationNames(result1); const executedMigrations = extractMigrationNames(result1);
// eslint-disable-next-line no-console
console.log(
"Executed migration select",
JSON.stringify(executedMigrations),
);
// Run pending migrations in order // Run pending migrations in order
for (const migration of this.migrations) { for (const migration of this.migrations) {
if (!executedMigrations.has(migration.name)) { if (!executedMigrations.has(migration.name)) {
const result2 = await sqlExec(migration.sql); await sqlExec(migration.sql);
// eslint-disable-next-line no-console
console.log("Executed migration", JSON.stringify(result2)); await sqlExec(
const result3 = await sqlExec(
`INSERT INTO migrations (name) VALUES ('${migration.name}')`, `INSERT INTO migrations (name) VALUES ('${migration.name}')`,
); );
// eslint-disable-next-line no-console
console.log("Updated migrations table", JSON.stringify(result3));
} }
} }
} }

12
src/services/platforms/CapacitorPlatformService.ts

@ -128,8 +128,6 @@ export class CapacitorPlatformService implements PlatformService {
let result: unknown; let result: unknown;
switch (operation.type) { switch (operation.type) {
case "run": { case "run": {
console.log("[CapacitorPlatformService] running sql:", operation.sql);
console.log("[CapacitorPlatformService] params:", operation.params);
const runResult = await this.db.run( const runResult = await this.db.run(
operation.sql, operation.sql,
operation.params, operation.params,
@ -141,8 +139,6 @@ export class CapacitorPlatformService implements PlatformService {
break; break;
} }
case "query": { case "query": {
console.log("[CapacitorPlatformService] querying sql:", operation.sql);
console.log("[CapacitorPlatformService] params:", operation.params);
const queryResult = await this.db.query( const queryResult = await this.db.query(
operation.sql, operation.sql,
operation.params, operation.params,
@ -158,15 +154,9 @@ export class CapacitorPlatformService implements PlatformService {
} }
operation.resolve(result); operation.resolve(result);
} catch (error) { } catch (error) {
// make sure you don't try to log to the DB... infinite loop! logger.error(
// eslint-disable-next-line no-console
console.error(
"[CapacitorPlatformService] Error while processing SQL queue:", "[CapacitorPlatformService] Error while processing SQL queue:",
error, error,
" ... for sql:",
operation.sql,
" ... with params:",
operation.params,
); );
operation.reject(error); operation.reject(error);
} }

3
src/views/AccountViewView.vue

@ -1119,7 +1119,6 @@ export default class AccountViewView extends Vue {
*/ */
async mounted() { async mounted() {
try { try {
console.log("[AccountViewView] mounted");
// Initialize component state with values from the database or defaults // Initialize component state with values from the database or defaults
await this.initializeState(); await this.initializeState();
await this.processIdentity(); await this.processIdentity();
@ -1172,7 +1171,6 @@ export default class AccountViewView extends Vue {
} }
} }
} catch (error) { } catch (error) {
console.log("[AccountViewView] error: ", JSON.stringify(error, null, 2));
// this can happen when running automated tests in dev mode because notifications don't work // this can happen when running automated tests in dev mode because notifications don't work
logger.error( logger.error(
"Telling user to clear cache at page create because:", "Telling user to clear cache at page create because:",
@ -1206,7 +1204,6 @@ export default class AccountViewView extends Vue {
this.turnOffNotifyingFlags(); this.turnOffNotifyingFlags();
} }
} }
// console.log("Got to the end of 'mounted' call in AccountViewView.");
/** /**
* Beware! I've seen where we never get to this point because "ready" never resolves. * Beware! I've seen where we never get to this point because "ready" never resolves.
*/ */

114
src/views/HomeView.vue

@ -521,7 +521,9 @@ export default class HomeView extends Vue {
logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`); logConsoleAndDb(`[HomeView] Retrieved ${this.allMyDids.length} DIDs`);
} catch (error) { } catch (error) {
logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true); logConsoleAndDb(`[HomeView] Failed to retrieve DIDs: ${error}`, true);
throw new Error("Failed to load existing identities. Please try restarting the app."); throw new Error(
"Failed to load existing identities. Please try restarting the app.",
);
} }
// Create new DID if needed // Create new DID if needed
@ -534,7 +536,10 @@ export default class HomeView extends Vue {
logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`); logConsoleAndDb(`[HomeView] Created new identity: ${newDid}`);
} catch (error) { } catch (error) {
this.isCreatingIdentifier = false; this.isCreatingIdentifier = false;
logConsoleAndDb(`[HomeView] Failed to create new identity: ${error}`, true); logConsoleAndDb(
`[HomeView] Failed to create new identity: ${error}`,
true,
);
throw new Error("Failed to create new identity. Please try again."); throw new Error("Failed to create new identity. Please try again.");
} }
} }
@ -546,34 +551,53 @@ export default class HomeView extends Vue {
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
settings = await retrieveSettingsForActiveAccount(); settings = await retrieveSettingsForActiveAccount();
} }
logConsoleAndDb(`[HomeView] Retrieved settings for ${settings.activeDid || 'no active DID'}`); logConsoleAndDb(
`[HomeView] Retrieved settings for ${settings.activeDid || "no active DID"}`,
);
} catch (error) { } catch (error) {
logConsoleAndDb(`[HomeView] Failed to retrieve settings: ${error}`, true); logConsoleAndDb(
throw new Error("Failed to load user settings. Some features may be limited."); `[HomeView] Failed to retrieve settings: ${error}`,
true,
);
throw new Error(
"Failed to load user settings. Some features may be limited.",
);
} }
// Update component state // Update component state
this.apiServer = settings.apiServer || ""; this.apiServer = settings.apiServer || "";
this.activeDid = settings.activeDid || ""; this.activeDid = settings.activeDid || "";
// Load contacts with graceful fallback // Load contacts with graceful fallback
try { try {
const platformService = PlatformServiceFactory.getInstance(); const platformService = PlatformServiceFactory.getInstance();
const dbContacts = await platformService.dbQuery("SELECT * FROM contacts"); const dbContacts = await platformService.dbQuery(
this.allContacts = databaseUtil.mapQueryResultToValues(dbContacts) as Contact[]; "SELECT * FROM contacts",
);
this.allContacts = databaseUtil.mapQueryResultToValues(
dbContacts,
) as Contact[];
if (USE_DEXIE_DB) { if (USE_DEXIE_DB) {
this.allContacts = await db.contacts.toArray(); this.allContacts = await db.contacts.toArray();
} }
logConsoleAndDb(`[HomeView] Retrieved ${this.allContacts.length} contacts`); logConsoleAndDb(
`[HomeView] Retrieved ${this.allContacts.length} contacts`,
);
} catch (error) { } catch (error) {
logConsoleAndDb(`[HomeView] Failed to retrieve contacts: ${error}`, true); logConsoleAndDb(
`[HomeView] Failed to retrieve contacts: ${error}`,
true,
);
this.allContacts = []; // Ensure we have a valid empty array this.allContacts = []; // Ensure we have a valid empty array
this.$notify({ this.$notify(
group: "alert", {
type: "warning", group: "alert",
title: "Contact Loading Issue", type: "warning",
text: "Some contact information may be unavailable.", title: "Contact Loading Issue",
}, 5000); text: "Some contact information may be unavailable.",
},
5000,
);
} }
// Update remaining settings // Update remaining settings
@ -583,14 +607,17 @@ export default class HomeView extends Vue {
this.isFeedFilteredByNearby = !!settings.filterFeedByNearby; this.isFeedFilteredByNearby = !!settings.filterFeedByNearby;
this.isRegistered = !!settings.isRegistered; this.isRegistered = !!settings.isRegistered;
this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId; this.lastAckedOfferToUserJwtId = settings.lastAckedOfferToUserJwtId;
this.lastAckedOfferToUserProjectsJwtId = settings.lastAckedOfferToUserProjectsJwtId; this.lastAckedOfferToUserProjectsJwtId =
settings.lastAckedOfferToUserProjectsJwtId;
this.searchBoxes = settings.searchBoxes || []; this.searchBoxes = settings.searchBoxes || [];
this.showShortcutBvc = !!settings.showShortcutBvc; this.showShortcutBvc = !!settings.showShortcutBvc;
this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings); this.isAnyFeedFilterOn = checkIsAnyFeedFilterOn(settings);
// Check onboarding status // Check onboarding status
if (!settings.finishedOnboarding) { if (!settings.finishedOnboarding) {
(this.$refs.onboardingDialog as OnboardingDialog).open(OnboardPage.Home); (this.$refs.onboardingDialog as OnboardingDialog).open(
OnboardPage.Home,
);
} }
// Check registration status if needed // Check registration status if needed
@ -613,10 +640,15 @@ export default class HomeView extends Vue {
}); });
} }
this.isRegistered = true; this.isRegistered = true;
logConsoleAndDb(`[HomeView] User ${this.activeDid} is now registered`); logConsoleAndDb(
`[HomeView] User ${this.activeDid} is now registered`,
);
} }
} catch (error) { } catch (error) {
logConsoleAndDb(`[HomeView] Registration check failed: ${error}`, true); logConsoleAndDb(
`[HomeView] Registration check failed: ${error}`,
true,
);
// Continue as unregistered - this is expected for new users // Continue as unregistered - this is expected for new users
} }
} }
@ -624,8 +656,11 @@ export default class HomeView extends Vue {
// Initialize feed and offers // Initialize feed and offers
try { try {
// Start feed update in background // Start feed update in background
this.updateAllFeed().catch(error => { this.updateAllFeed().catch((error) => {
logConsoleAndDb(`[HomeView] Background feed update failed: ${error}`, true); logConsoleAndDb(
`[HomeView] Background feed update failed: ${error}`,
true,
);
}); });
// Load new offers if we have an active DID // Load new offers if we have an active DID
@ -649,23 +684,28 @@ export default class HomeView extends Vue {
this.newOffersToUserHitLimit = offersToUser.hitLimit; this.newOffersToUserHitLimit = offersToUser.hitLimit;
this.numNewOffersToUserProjects = offersToProjects.data.length; this.numNewOffersToUserProjects = offersToProjects.data.length;
this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit; this.newOffersToUserProjectsHitLimit = offersToProjects.hitLimit;
logConsoleAndDb( logConsoleAndDb(
`[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` + `[HomeView] Retrieved ${this.numNewOffersToUser} user offers and ` +
`${this.numNewOffersToUserProjects} project offers` `${this.numNewOffersToUserProjects} project offers`,
); );
} }
} catch (error) { } catch (error) {
logConsoleAndDb(`[HomeView] Failed to initialize feed/offers: ${error}`, true); logConsoleAndDb(
`[HomeView] Failed to initialize feed/offers: ${error}`,
true,
);
// Don't throw - we can continue with empty feed // Don't throw - we can continue with empty feed
this.$notify({ this.$notify(
group: "alert", {
type: "warning", group: "alert",
title: "Feed Loading Issue", type: "warning",
text: "Some feed data may be unavailable. Pull to refresh.", title: "Feed Loading Issue",
}, 5000); text: "Some feed data may be unavailable. Pull to refresh.",
},
5000,
);
} }
} catch (error) { } catch (error) {
this.handleError(error); this.handleError(error);
throw error; // Re-throw to be caught by mounted() throw error; // Re-throw to be caught by mounted()
@ -837,10 +877,10 @@ export default class HomeView extends Vue {
private handleError(err: unknown) { private handleError(err: unknown) {
const errorMessage = err instanceof Error ? err.message : String(err); const errorMessage = err instanceof Error ? err.message : String(err);
const userMessage = (err as { userMessage?: string })?.userMessage; const userMessage = (err as { userMessage?: string })?.userMessage;
logConsoleAndDb( logConsoleAndDb(
`[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ''}`, `[HomeView] Initialization error: ${errorMessage}${userMessage ? ` (${userMessage})` : ""}`,
true true,
); );
this.$notify( this.$notify(
@ -848,7 +888,9 @@ export default class HomeView extends Vue {
group: "alert", group: "alert",
type: "danger", type: "danger",
title: "Error", title: "Error",
text: userMessage || "There was an error loading your data. Please try refreshing the page.", text:
userMessage ||
"There was an error loading your data. Please try refreshing the page.",
}, },
5000, 5000,
); );

4
src/views/IdentitySwitcherView.vue

@ -139,10 +139,8 @@ export default class IdentitySwitcherView extends Vue {
this.apiServerInput = settings.apiServer || ""; this.apiServerInput = settings.apiServer || "";
const accounts = await retrieveAllAccountsMetadata(); const accounts = await retrieveAllAccountsMetadata();
console.log("[IdentitySwitcherView] accounts: ", JSON.stringify(accounts, null, 2));
for (let n = 0; n < accounts.length; n++) { for (let n = 0; n < accounts.length; n++) {
const acct = accounts[n]; const acct = accounts[n];
console.log("[IdentitySwitcherView] acct: ", JSON.stringify(acct, null, 2));
this.otherIdentities.push({ this.otherIdentities.push({
id: (acct.id ?? 0).toString(), id: (acct.id ?? 0).toString(),
did: acct.did, did: acct.did,
@ -152,7 +150,6 @@ export default class IdentitySwitcherView extends Vue {
} }
} }
} catch (err) { } catch (err) {
console.log("[IdentitySwitcherView] error: ", JSON.stringify(err, null, 2));
this.$notify( this.$notify(
{ {
group: "alert", group: "alert",
@ -164,7 +161,6 @@ export default class IdentitySwitcherView extends Vue {
); );
logger.error("Telling user to clear cache at page create because:", err); logger.error("Telling user to clear cache at page create because:", err);
} }
console.log("[IdentitySwitcherView] end");
} }
async switchAccount(did?: string) { async switchAccount(did?: string) {

7
src/views/ProjectViewView.vue

@ -389,7 +389,12 @@
{{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }} {{ libsUtil.formattedAmount(givenTotalHours(), "HUR") }}
</span> </span>
<span v-else> <span v-else>
{{ libsUtil.formattedAmount(givesTotalsByUnit[0].amount, givesTotalsByUnit[0].unit) }} {{
libsUtil.formattedAmount(
givesTotalsByUnit[0].amount,
givesTotalsByUnit[0].unit,
)
}}
</span> </span>
<span v-if="givesTotalsByUnit.length > 1">...</span> <span v-if="givesTotalsByUnit.length > 1">...</span>
<span> <span>

Loading…
Cancel
Save