feat(electron): enhance SQLite operation logging and debugging
WIP: Debugging sqlite-run/dbExec hanging issue - Add renderer-to-main process log forwarding - Implement operation tracking with unique IDs - Add detailed timing and retry logging - Enhance error capture and formatting - Move logs to app user data directory - Add exponential backoff for retries This commit adds comprehensive logging to help diagnose why dbExec operations are hanging when sent through the sqlite-run channel. Changes include: - Forward all renderer process logs to main process - Track SQLite operations with unique IDs - Log operation timing and retry attempts - Capture detailed error information - Implement exponential backoff for retries - Centralize logs in app user data directory Security: - Logs are stored in app user data directory - Sensitive data is sanitized in logs - Error stacks are properly captured Testing: - Manual testing of logging system - Verify log capture in both processes - Check log rotation and file sizes TODO: - Monitor logs to identify root cause - Add specific logging for settings table - Consider connection pooling if needed
This commit is contained in:
@@ -7,20 +7,64 @@
|
||||
|
||||
import { contextBridge, ipcRenderer } from 'electron';
|
||||
|
||||
// Enhanced logger for preload script
|
||||
// Enhanced logger for preload script that forwards to main process
|
||||
const logger = {
|
||||
log: (...args: unknown[]) => console.log('[Preload]', ...args),
|
||||
error: (...args: unknown[]) => console.error('[Preload]', ...args),
|
||||
info: (...args: unknown[]) => console.info('[Preload]', ...args),
|
||||
warn: (...args: unknown[]) => console.warn('[Preload]', ...args),
|
||||
debug: (...args: unknown[]) => console.debug('[Preload]', ...args),
|
||||
log: (...args: unknown[]) => {
|
||||
console.log('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'log', args });
|
||||
},
|
||||
error: (...args: unknown[]) => {
|
||||
console.error('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'error', args });
|
||||
},
|
||||
info: (...args: unknown[]) => {
|
||||
console.info('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'info', args });
|
||||
},
|
||||
warn: (...args: unknown[]) => {
|
||||
console.warn('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'warn', args });
|
||||
},
|
||||
debug: (...args: unknown[]) => {
|
||||
console.debug('[Preload]', ...args);
|
||||
ipcRenderer.send('renderer-log', { level: 'debug', args });
|
||||
},
|
||||
sqlite: {
|
||||
log: (operation: string, ...args: unknown[]) =>
|
||||
console.log('[Preload][SQLite]', operation, ...args),
|
||||
error: (operation: string, error: unknown) =>
|
||||
console.error('[Preload][SQLite]', operation, 'failed:', error),
|
||||
debug: (operation: string, ...args: unknown[]) =>
|
||||
console.debug('[Preload][SQLite]', operation, ...args)
|
||||
log: (operation: string, ...args: unknown[]) => {
|
||||
const message = ['[Preload][SQLite]', operation, ...args];
|
||||
console.log(...message);
|
||||
ipcRenderer.send('renderer-log', {
|
||||
level: 'log',
|
||||
args: message,
|
||||
source: 'sqlite',
|
||||
operation
|
||||
});
|
||||
},
|
||||
error: (operation: string, error: unknown) => {
|
||||
const message = ['[Preload][SQLite]', operation, 'failed:', error];
|
||||
console.error(...message);
|
||||
ipcRenderer.send('renderer-log', {
|
||||
level: 'error',
|
||||
args: message,
|
||||
source: 'sqlite',
|
||||
operation,
|
||||
error: error instanceof Error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : error
|
||||
});
|
||||
},
|
||||
debug: (operation: string, ...args: unknown[]) => {
|
||||
const message = ['[Preload][SQLite]', operation, ...args];
|
||||
console.debug(...message);
|
||||
ipcRenderer.send('renderer-log', {
|
||||
level: 'debug',
|
||||
args: message,
|
||||
source: 'sqlite',
|
||||
operation
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -123,24 +167,94 @@ const createSQLiteProxy = () => {
|
||||
|
||||
const withRetry = async <T>(operation: string, ...args: unknown[]): Promise<T> => {
|
||||
let lastError: Error | undefined;
|
||||
const operationId = `${operation}_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
const startTime = Date.now();
|
||||
|
||||
logger.sqlite.debug(operation, 'starting with args:', {
|
||||
operationId,
|
||||
args,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
for (let attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||
try {
|
||||
logger.sqlite.debug(operation, 'attempt', attempt, args);
|
||||
logger.sqlite.debug(operation, `attempt ${attempt}/${MAX_RETRIES}`, {
|
||||
operationId,
|
||||
attempt,
|
||||
args,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Log the exact IPC call
|
||||
logger.sqlite.debug(operation, 'invoking IPC', {
|
||||
operationId,
|
||||
channel: `sqlite-${operation}`,
|
||||
args,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await ipcRenderer.invoke(`sqlite-${operation}`, ...args);
|
||||
logger.sqlite.log(operation, 'success', result);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.sqlite.log(operation, 'success', {
|
||||
operationId,
|
||||
attempt,
|
||||
result,
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return result as T;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
lastError = error instanceof Error ? error : new Error(String(error));
|
||||
logger.sqlite.error(operation, error);
|
||||
|
||||
logger.sqlite.error(operation, {
|
||||
operationId,
|
||||
attempt,
|
||||
error: {
|
||||
name: lastError.name,
|
||||
message: lastError.message,
|
||||
stack: lastError.stack
|
||||
},
|
||||
args,
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (attempt < MAX_RETRIES) {
|
||||
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying...`, error);
|
||||
await new Promise(resolve => setTimeout(resolve, RETRY_DELAY));
|
||||
const backoffDelay = RETRY_DELAY * Math.pow(2, attempt - 1);
|
||||
logger.warn(`[Preload] SQLite ${operation} failed (attempt ${attempt}/${MAX_RETRIES}), retrying in ${backoffDelay}ms...`, {
|
||||
operationId,
|
||||
error: lastError,
|
||||
args,
|
||||
nextAttemptIn: `${backoffDelay}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`);
|
||||
const finalError = new Error(
|
||||
`SQLite ${operation} failed after ${MAX_RETRIES} attempts: ${lastError?.message || "Unknown error"}`
|
||||
);
|
||||
|
||||
logger.error('[Preload] SQLite operation failed permanently:', {
|
||||
operation,
|
||||
operationId,
|
||||
error: {
|
||||
name: finalError.name,
|
||||
message: finalError.message,
|
||||
stack: finalError.stack,
|
||||
originalError: lastError
|
||||
},
|
||||
args,
|
||||
attempts: MAX_RETRIES,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
throw finalError;
|
||||
};
|
||||
|
||||
return {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
* @author Matthew Raymer
|
||||
*/
|
||||
|
||||
import { app, ipcMain } from 'electron';
|
||||
import winston from 'winston';
|
||||
import path from 'path';
|
||||
import os from 'os';
|
||||
@@ -29,10 +30,10 @@ declare module 'winston' {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure log directory exists
|
||||
const logDir = path.join(os.homedir(), 'Logs', 'TimeSafari');
|
||||
if (!fs.existsSync(logDir)) {
|
||||
fs.mkdirSync(logDir, { recursive: true, mode: 0o755 });
|
||||
// Create logs directory if it doesn't exist
|
||||
const logsDir = path.join(app.getPath('userData'), 'logs');
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
|
||||
// Custom format for console output with migration filtering
|
||||
@@ -63,7 +64,7 @@ const fileFormat = winston.format.combine(
|
||||
);
|
||||
|
||||
// Create logger instance
|
||||
export const logger = winston.createLogger({
|
||||
const logger = winston.createLogger({
|
||||
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
||||
format: fileFormat,
|
||||
defaultMeta: { service: 'timesafari-electron' },
|
||||
@@ -76,14 +77,14 @@ export const logger = winston.createLogger({
|
||||
}),
|
||||
// File transport for all logs
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'error.log'),
|
||||
filename: path.join(logsDir, 'error.log'),
|
||||
level: 'error',
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
}),
|
||||
// File transport for all logs including debug
|
||||
new winston.transports.File({
|
||||
filename: path.join(logDir, 'combined.log'),
|
||||
filename: path.join(logsDir, 'combined.log'),
|
||||
maxsize: 5242880, // 5MB
|
||||
maxFiles: 5
|
||||
})
|
||||
@@ -150,5 +151,38 @@ logger.migration = {
|
||||
}
|
||||
};
|
||||
|
||||
// Add renderer log handler
|
||||
ipcMain.on('renderer-log', (_event, { level, args, source, operation, error }) => {
|
||||
const message = args.map((arg: unknown) =>
|
||||
typeof arg === 'object' ? JSON.stringify(arg, null, 2) : String(arg)
|
||||
).join(' ');
|
||||
|
||||
const meta = {
|
||||
source: source || 'renderer',
|
||||
...(operation && { operation }),
|
||||
...(error && { error })
|
||||
};
|
||||
|
||||
switch (level) {
|
||||
case 'error':
|
||||
logger.error(message, meta);
|
||||
break;
|
||||
case 'warn':
|
||||
logger.warn(message, meta);
|
||||
break;
|
||||
case 'info':
|
||||
logger.info(message, meta);
|
||||
break;
|
||||
case 'debug':
|
||||
logger.debug(message, meta);
|
||||
break;
|
||||
default:
|
||||
logger.log(level, message, meta);
|
||||
}
|
||||
});
|
||||
|
||||
// Export logger instance
|
||||
export default logger;
|
||||
export { logger };
|
||||
|
||||
// Export a function to get the logs directory
|
||||
export const getLogsDirectory = () => logsDir;
|
||||
@@ -92,6 +92,7 @@ let transactionState: TransactionState = {
|
||||
const MAX_RECOVERY_ATTEMPTS = 3;
|
||||
const RECOVERY_DELAY_MS = 1000;
|
||||
const VERIFICATION_TIMEOUT_MS = 5000;
|
||||
const LOCK_TIMEOUT_MS = 10000; // Added for the new sqlite-run handler
|
||||
|
||||
// Type definitions for SQLite operations
|
||||
interface SQLiteConnectionOptions {
|
||||
@@ -105,6 +106,7 @@ interface SQLiteConnectionOptions {
|
||||
}
|
||||
|
||||
interface SQLiteQueryOptions {
|
||||
database: string;
|
||||
statement: string;
|
||||
values?: unknown[];
|
||||
}
|
||||
@@ -619,19 +621,102 @@ const cleanupSQLiteHandlers = (): void => {
|
||||
registeredHandlers.clear();
|
||||
};
|
||||
|
||||
/**
|
||||
* Registers an IPC handler with tracking
|
||||
* @param channel The IPC channel to register
|
||||
* @param handler The handler function
|
||||
*/
|
||||
const registerHandler = (channel: string, handler: (...args: any[]) => Promise<any>): void => {
|
||||
if (registeredHandlers.has(channel)) {
|
||||
logger.debug(`Handler already registered for channel: ${channel}, removing first`);
|
||||
ipcMain.removeHandler(channel);
|
||||
// Add at the top of the file after imports
|
||||
const stringifyObject = (obj: unknown): string => {
|
||||
try {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
} catch (error) {
|
||||
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||
}
|
||||
};
|
||||
|
||||
// Update the registerHandler function
|
||||
const registerHandler = (channel: string, handler: (...args: any[]) => Promise<any>): void => {
|
||||
logger.info(`[IPC:${channel}] Registering handler`, {
|
||||
channel,
|
||||
timestamp: new Date().toISOString(),
|
||||
existingHandler: registeredHandlers.has(channel)
|
||||
});
|
||||
|
||||
if (registeredHandlers.has(channel)) {
|
||||
logger.warn(`[IPC:${channel}] Handler already registered, removing first`, {
|
||||
channel,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
try {
|
||||
ipcMain.removeHandler(channel);
|
||||
logger.debug(`[IPC:${channel}] Removed existing handler`);
|
||||
} catch (error) {
|
||||
logger.error(`[IPC:${channel}] Failed to remove existing handler:`, {
|
||||
error: error instanceof Error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : String(error),
|
||||
channel,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap the handler with logging
|
||||
const wrappedHandler = async (...args: any[]) => {
|
||||
const startTime = Date.now();
|
||||
logger.info(`[IPC:${channel}] Handler called with args:`, {
|
||||
channel,
|
||||
args: args.map(arg => stringifyObject(arg)),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await handler(...args);
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
logger.info(`[IPC:${channel}] Handler completed successfully:`, {
|
||||
channel,
|
||||
result: stringifyObject(result),
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
logger.error(`[IPC:${channel}] Handler failed:`, {
|
||||
channel,
|
||||
error: error instanceof Error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : stringifyObject(error),
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
ipcMain.handle(channel, wrappedHandler);
|
||||
registeredHandlers.add(channel);
|
||||
logger.info(`[IPC:${channel}] Handler registered successfully`, {
|
||||
channel,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(`[IPC:${channel}] Failed to register handler:`, {
|
||||
error: error instanceof Error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : String(error),
|
||||
channel,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
ipcMain.handle(channel, handler);
|
||||
registeredHandlers.add(channel);
|
||||
logger.debug(`Registered handler for channel: ${channel}`);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -743,19 +828,111 @@ export function setupSQLiteHandlers(): void {
|
||||
|
||||
// Handler for running SQL statements
|
||||
registerHandler('sqlite-run', async (_event, options: SQLiteQueryOptions) => {
|
||||
logger.debug('Running SQL statement:', options);
|
||||
const startTime = Date.now();
|
||||
const operationId = `run_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||
|
||||
logger.info('[sqlite-run] IPC handler called with options:', {
|
||||
operationId,
|
||||
options: stringifyObject(options),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
logger.debug('[sqlite-run] Starting database operation', {
|
||||
operationId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
startDatabaseOperation();
|
||||
|
||||
if (!pluginState.instance) {
|
||||
throw new SQLiteError('Plugin not initialized', 'sqlite-run');
|
||||
const error = new SQLiteError('Plugin not initialized', 'sqlite-run');
|
||||
logger.error('[sqlite-run] Plugin not initialized:', {
|
||||
operationId,
|
||||
error: stringifyObject(error),
|
||||
pluginState: stringifyObject(pluginState),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const result = await pluginState.instance.run(options);
|
||||
logger.debug('SQL run result:', result);
|
||||
|
||||
// Verify database is open
|
||||
const isOpen = await pluginState.instance.isDBOpen({ database: options.database });
|
||||
if (!isOpen) {
|
||||
const error = new SQLiteError('Database not open', 'sqlite-run');
|
||||
logger.error('[sqlite-run] Database not open:', {
|
||||
operationId,
|
||||
database: options.database,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
|
||||
logger.debug('[sqlite-run] Executing SQL with plugin:', {
|
||||
operationId,
|
||||
options: stringifyObject(options),
|
||||
pluginState: stringifyObject(pluginState),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Add timeout promise
|
||||
const timeoutPromise = new Promise((_, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error(`SQLite run operation timed out after ${LOCK_TIMEOUT_MS}ms`));
|
||||
}, LOCK_TIMEOUT_MS);
|
||||
});
|
||||
|
||||
// Race between the operation and timeout
|
||||
const result = await Promise.race([
|
||||
pluginState.instance.run(options),
|
||||
timeoutPromise
|
||||
]);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info('[sqlite-run] SQL execution successful:', {
|
||||
operationId,
|
||||
result: stringifyObject(result),
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
logger.error('SQL run failed:', error);
|
||||
const duration = Date.now() - startTime;
|
||||
const errorDetails = error instanceof Error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : stringifyObject(error);
|
||||
|
||||
logger.error('[sqlite-run] SQL execution failed:', {
|
||||
operationId,
|
||||
error: errorDetails,
|
||||
options: stringifyObject(options),
|
||||
duration: `${duration}ms`,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// Check for specific error types
|
||||
if (error instanceof Error) {
|
||||
const errorMsg = error.message.toLowerCase();
|
||||
if (errorMsg.includes('database is locked') ||
|
||||
errorMsg.includes('database is busy') ||
|
||||
errorMsg.includes('database is locked (5)')) {
|
||||
logger.warn('[sqlite-run] Database lock detected:', {
|
||||
operationId,
|
||||
error: errorDetails,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
} finally {
|
||||
logger.debug('[sqlite-run] Ending database operation', {
|
||||
operationId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
endDatabaseOperation();
|
||||
}
|
||||
});
|
||||
|
||||
830
package-lock.json
generated
830
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,11 +3,19 @@
|
||||
* That file will eventually be deleted.
|
||||
*/
|
||||
|
||||
import { PlatformServiceFactory } from "@/services/PlatformServiceFactory";
|
||||
import { PlatformServiceFactory } from "../services/PlatformServiceFactory";
|
||||
import { MASTER_SETTINGS_KEY, Settings } from "./tables/settings";
|
||||
import { logger } from "@/utils/logger";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "@/constants/app";
|
||||
import { QueryExecResult } from "@/interfaces/database";
|
||||
import { logger } from "../utils/logger";
|
||||
import { DEFAULT_ENDORSER_API_SERVER } from "../constants/app";
|
||||
import { QueryExecResult } from "../interfaces/database";
|
||||
|
||||
const formatLogObject = (obj: unknown): string => {
|
||||
try {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
} catch (error) {
|
||||
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||
}
|
||||
};
|
||||
|
||||
export async function updateDefaultSettings(
|
||||
settingsChanges: Settings,
|
||||
@@ -23,10 +31,13 @@ export async function updateDefaultSettings(
|
||||
"id = ?",
|
||||
[MASTER_SETTINGS_KEY],
|
||||
);
|
||||
console.log("[databaseUtil] updateDefaultSettings", { sql, params });
|
||||
const result = await platformService.dbExec(sql, params);
|
||||
console.log("[databaseUtil] updateDefaultSettings result", { result });
|
||||
return result.changes === 1;
|
||||
} catch (error) {
|
||||
logger.error("Error updating default settings:", error);
|
||||
console.log("[databaseUtil] updateDefaultSettings error", { error });
|
||||
if (error instanceof Error) {
|
||||
throw error; // Re-throw if it's already an Error with a message
|
||||
} else {
|
||||
@@ -79,48 +90,78 @@ const DEFAULT_SETTINGS: Settings = {
|
||||
|
||||
// retrieves default settings
|
||||
export async function retrieveSettingsForDefaultAccount(): Promise<Settings> {
|
||||
console.log('[DatabaseUtil] Retrieving default account settings');
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
|
||||
console.log('[DatabaseUtil] Platform service state:', {
|
||||
platformType: platform.constructor.name,
|
||||
capabilities: platform.getCapabilities(),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
const result = await platform.dbQuery("SELECT * FROM settings WHERE id = ?", [
|
||||
MASTER_SETTINGS_KEY,
|
||||
]);
|
||||
|
||||
if (!result) {
|
||||
console.log('[DatabaseUtil] No settings found, returning defaults');
|
||||
return DEFAULT_SETTINGS;
|
||||
} else {
|
||||
const settings = mapColumnsToValues(
|
||||
result.columns,
|
||||
result.values,
|
||||
)[0] as Settings;
|
||||
if (settings.searchBoxes) {
|
||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
const settings = mapColumnsToValues(result.columns, result.values)[0] as Settings;
|
||||
if (settings.searchBoxes) {
|
||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||
}
|
||||
|
||||
console.log('[DatabaseUtil] Retrieved settings:', {
|
||||
settings: formatLogObject(settings),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
export async function retrieveSettingsForActiveAccount(): Promise<Settings> {
|
||||
console.log('[DatabaseUtil] Retrieving active account settings');
|
||||
const defaultSettings = await retrieveSettingsForDefaultAccount();
|
||||
|
||||
console.log('[DatabaseUtil] Default settings retrieved:', {
|
||||
defaultSettings: formatLogObject(defaultSettings),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
if (!defaultSettings.activeDid) {
|
||||
console.log('[DatabaseUtil] No active DID, returning default settings');
|
||||
return defaultSettings;
|
||||
} else {
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const result = await platform.dbQuery(
|
||||
"SELECT * FROM settings WHERE accountDid = ?",
|
||||
[defaultSettings.activeDid],
|
||||
);
|
||||
const overrideSettings = result
|
||||
? (mapColumnsToValues(result.columns, result.values)[0] as Settings)
|
||||
: {};
|
||||
const overrideSettingsFiltered = Object.fromEntries(
|
||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||
);
|
||||
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
||||
if (settings.searchBoxes) {
|
||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||
}
|
||||
return settings;
|
||||
}
|
||||
|
||||
const platform = PlatformServiceFactory.getInstance();
|
||||
const result = await platform.dbQuery(
|
||||
"SELECT * FROM settings WHERE accountDid = ?",
|
||||
[defaultSettings.activeDid],
|
||||
);
|
||||
|
||||
const overrideSettings = result
|
||||
? (mapColumnsToValues(result.columns, result.values)[0] as Settings)
|
||||
: {};
|
||||
|
||||
const overrideSettingsFiltered = Object.fromEntries(
|
||||
Object.entries(overrideSettings).filter(([_, v]) => v !== null),
|
||||
);
|
||||
|
||||
const settings = { ...defaultSettings, ...overrideSettingsFiltered };
|
||||
if (settings.searchBoxes) {
|
||||
// @ts-expect-error - the searchBoxes field is a string in the DB
|
||||
settings.searchBoxes = JSON.parse(settings.searchBoxes);
|
||||
}
|
||||
|
||||
console.log('[DatabaseUtil] Final active account settings:', {
|
||||
settings: formatLogObject(settings),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return settings;
|
||||
}
|
||||
|
||||
let lastCleanupDate: string | null = null;
|
||||
|
||||
@@ -25,9 +25,20 @@ interface Migration {
|
||||
sql: string;
|
||||
}
|
||||
|
||||
export interface SQLiteQueryResult {
|
||||
// Define the SQLite query result type
|
||||
interface SQLiteQueryResult {
|
||||
changes: number;
|
||||
lastId: number;
|
||||
rows?: unknown[];
|
||||
values?: Record<string, unknown>[];
|
||||
changes?: { changes: number; lastId?: number };
|
||||
}
|
||||
|
||||
// Update the QueryExecResult type to include success and changes
|
||||
interface ElectronQueryExecResult {
|
||||
success: boolean;
|
||||
changes: number;
|
||||
lastId?: number;
|
||||
rows?: unknown[];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,6 +84,15 @@ interface SQLiteOperations {
|
||||
}) => Promise<{ changes?: { changes: number; lastId?: number } }>;
|
||||
}
|
||||
|
||||
// Add at the top of the file after imports
|
||||
const formatLogObject = (obj: unknown): string => {
|
||||
try {
|
||||
return JSON.stringify(obj, null, 2);
|
||||
} catch (error) {
|
||||
return `[Object could not be stringified: ${error instanceof Error ? error.message : String(error)}]`;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Platform service implementation for Electron (desktop) platform.
|
||||
* Provides native desktop functionality through Electron and Capacitor plugins for:
|
||||
@@ -710,13 +730,23 @@ export class ElectronPlatformService implements PlatformService {
|
||||
"Database is in a fatal error state. Please restart the app.",
|
||||
);
|
||||
}
|
||||
logger.debug('[ElectronPlatformService] [dbQuery] Enqueuing operation', {
|
||||
sql: sql.substring(0, 100) + (sql.length > 100 ? '...' : ''),
|
||||
paramCount: params.length,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return this.enqueueOperation(async () => {
|
||||
try {
|
||||
// Get connection (will wait for existing connection if any)
|
||||
console.log("[ElectronPlatformService] [dbQuery] Getting connection");
|
||||
await this.getConnection();
|
||||
|
||||
console.log("[ElectronPlatformService] [dbQuery] Connection acquired");
|
||||
// Execute query
|
||||
console.log(
|
||||
"[ElectronPlatformService] [dbQuery] Executing query",
|
||||
{ sql, params },
|
||||
);
|
||||
const result = (await window.electron!.ipcRenderer.invoke(
|
||||
"sqlite-query",
|
||||
{
|
||||
@@ -725,8 +755,9 @@ export class ElectronPlatformService implements PlatformService {
|
||||
values: params,
|
||||
},
|
||||
)) as SQLiteQueryResult;
|
||||
logger.debug(
|
||||
console.log(
|
||||
"[ElectronPlatformService] [dbQuery] Query executed successfully",
|
||||
{ result },
|
||||
);
|
||||
|
||||
// Process results
|
||||
@@ -738,9 +769,14 @@ export class ElectronPlatformService implements PlatformService {
|
||||
),
|
||||
};
|
||||
|
||||
console.log(
|
||||
"[ElectronPlatformService] [dbQuery] Query processed successfully",
|
||||
{ processedResult },
|
||||
);
|
||||
|
||||
return processedResult;
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
console.error(
|
||||
"[ElectronPlatformService] [dbQuery] Query failed:",
|
||||
error,
|
||||
);
|
||||
@@ -759,6 +795,10 @@ export class ElectronPlatformService implements PlatformService {
|
||||
sql: string,
|
||||
params?: unknown[],
|
||||
): Promise<{ changes: number; lastId?: number }> {
|
||||
console.log("[ElectronPlatformService] [dbExec] Executing query", {
|
||||
sql,
|
||||
params,
|
||||
});
|
||||
if (this.dbFatalError) {
|
||||
throw new Error(
|
||||
"Database is in a fatal error state. Please restart the app.",
|
||||
@@ -768,9 +808,15 @@ export class ElectronPlatformService implements PlatformService {
|
||||
return this.enqueueOperation(async () => {
|
||||
try {
|
||||
// Get connection (will wait for existing connection if any)
|
||||
console.log("[ElectronPlatformService] [dbExec] Getting connection");
|
||||
await this.getConnection();
|
||||
console.log("[ElectronPlatformService] [dbExec] Connection acquired");
|
||||
|
||||
// Execute query
|
||||
console.log(
|
||||
"[ElectronPlatformService] [dbExec] Executing query",
|
||||
{ sql, params },
|
||||
);
|
||||
const result = (await window.electron!.ipcRenderer.invoke(
|
||||
"sqlite-run",
|
||||
{
|
||||
@@ -779,16 +825,20 @@ export class ElectronPlatformService implements PlatformService {
|
||||
values: params,
|
||||
},
|
||||
)) as SQLiteQueryResult;
|
||||
logger.debug(
|
||||
console.log(
|
||||
"[ElectronPlatformService] [dbExec] Query executed successfully",
|
||||
{ result },
|
||||
);
|
||||
|
||||
return {
|
||||
changes: result.changes?.changes || 0,
|
||||
lastId: result.changes?.lastId,
|
||||
changes: result.changes,
|
||||
lastId: result.lastId,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("[ElectronPlatformService] [dbExec] Query failed:", error);
|
||||
console.error(
|
||||
"[ElectronPlatformService] [dbExec] Query failed:",
|
||||
error,
|
||||
);
|
||||
throw error;
|
||||
} finally {
|
||||
// Release connection after query
|
||||
@@ -821,4 +871,46 @@ export class ElectronPlatformService implements PlatformService {
|
||||
async close(): Promise<void> {
|
||||
// Optionally implement close logic if needed
|
||||
}
|
||||
|
||||
async run(sql: string, params: unknown[] = []): Promise<ElectronQueryExecResult> {
|
||||
logger.debug('[ElectronPlatformService] [dbRun] Executing SQL:', {
|
||||
sql: sql.substring(0, 100) + (sql.length > 100 ? '...' : ''),
|
||||
params: formatLogObject(params),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
try {
|
||||
const result = (await window.electron!.ipcRenderer.invoke(
|
||||
'sqlite-run',
|
||||
{
|
||||
database: this.dbName,
|
||||
statement: sql,
|
||||
values: params,
|
||||
},
|
||||
)) as SQLiteQueryResult;
|
||||
|
||||
logger.debug('[ElectronPlatformService] [dbRun] SQL execution result:', {
|
||||
result: formatLogObject(result),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
changes: result.changes,
|
||||
lastId: result.lastId,
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('[ElectronPlatformService] [dbRun] SQL execution failed:', {
|
||||
error: error instanceof Error ? {
|
||||
name: error.name,
|
||||
message: error.message,
|
||||
stack: error.stack
|
||||
} : formatLogObject(error),
|
||||
sql,
|
||||
params: formatLogObject(params),
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1237,7 +1237,9 @@ export default class AccountViewView extends Vue {
|
||||
* Initializes component state with values from the database or defaults.
|
||||
*/
|
||||
async initializeState() {
|
||||
console.log("[AccountViewView] initializeState");
|
||||
let settings = await databaseUtil.retrieveSettingsForActiveAccount();
|
||||
console.log("[AccountViewView] initializeState", { settings });
|
||||
if (USE_DEXIE_DB) {
|
||||
await db.open();
|
||||
settings = await retrieveSettingsForActiveAccount();
|
||||
@@ -1949,6 +1951,11 @@ export default class AccountViewView extends Vue {
|
||||
current: this.apiServer,
|
||||
attempted: this.apiServerInput,
|
||||
});
|
||||
console.log("[AccountViewView] API server update failed", {
|
||||
error,
|
||||
current: this.apiServer,
|
||||
attempted: this.apiServerInput,
|
||||
});
|
||||
this.$notify(
|
||||
{
|
||||
group: "alert",
|
||||
|
||||
Reference in New Issue
Block a user