/** * Log message transformation utilities for human-readable display. * * Transforms technical log messages into user-friendly descriptions * while preserving technical details for the expanded view. */ /** Function that transforms a log message using details context */ type MessageTransformer = (details: Record) => string; /** * Message templates for known log patterns. * Keys are matched against the start of log messages (case-insensitive). */ const MESSAGE_TEMPLATES: Record = { // Meeting lifecycle 'meeting stopped': (d) => { const title = d.title || d.meeting_title; return title ? `Meeting "${title}" has ended` : 'Meeting has ended'; }, 'meeting started': (d) => { const title = d.title || d.meeting_title; return title ? `Started recording "${title}"` : 'Started recording'; }, 'recording start failed': (d) => { const reason = d.error || d.reason; return reason ? `Recording failed to start: ${reason}` : 'Recording failed to start'; }, 'created meeting': (d) => { const title = d.title || d.meeting_title; return title ? `Created new meeting: ${title}` : 'Created new meeting'; }, 'meeting deleted': () => 'Meeting deleted successfully', stopmeetingrpc: () => 'Meeting recording has been stopped', // Cloud consent 'cloud consent granted': () => 'Cloud AI features enabled', 'cloud consent revoked': () => 'Cloud AI features disabled', // Summarization 'summary generated': (d) => { const model = d.model || d.provider; return model ? `Summary ready (${model})` : 'Summary generated successfully'; }, summarizing: (d) => { const count = d.segment_count || d.segments; return count ? `Generating summary from ${count} segments...` : 'Generating summary...'; }, 'summary generation failed': (d) => { const reason = d.error || d.reason; return reason ? `Summary generation failed: ${reason}` : 'Summary generation failed'; }, // Transcription 'segment processed': (d) => { const { text } = d; if (text && text.length > 50) { return `Transcribed: "${text.substring(0, 50)}..."`; } return text ? `Transcribed: "${text}"` : 'Segment transcribed'; }, 'transcript export completed': (d) => { const fmt = d.format; return fmt ? `Transcript exported as ${fmt.toUpperCase()}` : 'Transcript exported'; }, 'starting transcript export': (d) => { const fmt = d.format; return fmt ? `Exporting transcript as ${fmt.toUpperCase()}...` : 'Exporting transcript...'; }, // Diarization 'diarization job': (d) => { const { status } = d; if (status === 'completed') { return 'Speaker identification complete'; } if (status === 'running' || status === 'processing') { return 'Identifying speakers...'; } if (status === 'failed') { return 'Speaker identification failed'; } return 'Speaker identification in progress'; }, refinespeak: () => 'Refining speaker labels...', 'speaker renamed': (d) => { const from = d.old_name || d.from; const to = d.new_name || d.to; if (from && to) { return `Renamed speaker "${from}" to "${to}"`; } return 'Speaker renamed'; }, // Triggers 'trigger snooze cleared': () => 'Recording prompt re-enabled', 'trigger detected': (d) => { const type = d.trigger_type || d.type; if (type === 'calendar') { return 'Calendar meeting starting soon'; } if (type === 'audio') { return 'Audio activity detected'; } return 'Recording trigger detected'; }, // Webhooks webhook_registered: (d) => { const name = d.name || d.webhook_name; return name ? `Webhook "${name}" registered` : 'Webhook registered'; }, webhook_delivered: (d) => { const event = d.event_type; return event ? `Webhook delivered: ${event}` : 'Webhook delivered successfully'; }, webhook_failed: (d) => { const reason = d.error; return reason ? `Webhook delivery failed: ${reason}` : 'Webhook delivery failed'; }, // Calendar 'calendar sync': (d) => { const { provider } = d; const count = d.count || d.events_synced; if (count) { return `Synced ${count} calendar events${provider ? ` from ${provider}` : ''}`; } return provider ? `Calendar synced with ${provider}` : 'Calendar synced'; }, // Entity extraction extracted: (d) => { const count = d.count || d.entity_count; return count ? `Extracted ${count} entities from transcript` : 'Entities extracted'; }, // Recovery 'recovered crashed meeting': () => 'Recovered meeting from unexpected shutdown', 'crash recovery complete': (d) => { const count = d.meetings_recovered || d.count; return count ? `Recovery complete: ${count} meeting(s) restored` : 'Recovery complete'; }, // Connection 'connection timeout': () => 'Server connection timed out', 'connection failed': () => 'Failed to connect to server', connected: () => 'Connected to server', disconnected: () => 'Disconnected from server', // Server 'server started': (d) => { const { port } = d; return port ? `Server running on port ${port}` : 'Server started'; }, 'server stopped': () => 'Server stopped', // RPC operations (generic) rpc: (d) => { const { method, status } = d; if (method && status === 'OK') { const simpleName = method.split('/').pop() || method; return `${simpleName} completed`; } return 'Operation completed'; }, // Audio device management 'audio device hydration complete': () => 'Audio device settings loaded', 'audio device prefs synced': () => 'Audio device settings synced', 'hydration: audio_devices synced': () => 'Audio devices restored from saved settings', 'loaddevices: stored preferences': () => 'Loaded saved audio device preferences', 'audio device selected': (d) => { const deviceType = d.device_type; const deviceName = d.device_name || 'device'; if (deviceType === 'input') { return `Selected microphone: ${deviceName}`; } if (deviceType === 'output') { return `Selected speaker: ${deviceName}`; } return `Selected audio device: ${deviceName}`; }, 'audio device preferences reset': () => 'Audio device settings reset to defaults', 'skipping output auto-select': () => 'Audio output selection waiting for user choice', // Preferences and persistence 'preferences persistence recovered': () => 'Settings now saving correctly', 'failed to persist preferences': () => 'Could not save settings to disk', 'preferences may not persist': () => 'Settings may not be saved when app closes', 'hydration failed': () => 'Using browser-cached settings (disk settings unavailable)', 'replacing preferences': () => 'Updating settings', 'server address override': () => 'Custom server address configured', // API and adapter status 'adapter fallback': () => 'Working in offline mode', 'event bridge initialization failed': () => 'Event system starting with limited features', 'tauri stream send_audio_chunk failed': () => 'Audio streaming interrupted', 'tauri stream stop_recording failed': () => 'Could not stop recording cleanly', 'stream state force-reset': () => 'Recording state reset', 'preflight connect failed': () => 'Connection check failed before recording', 'stream state check failed': () => 'Recording check failed - proceeding with protection', // Reconnection 'server info fetch failed': () => 'Server version check skipped', 'integration revalidation failed': () => 'Reconnected - some integrations may need refresh', 'reconnection callback execution failed': () => 'Reconnection partially completed', 'state sync after reconnect failed': () => 'Sync incomplete after reconnecting', 'scheduled reconnect attempt failed': (d) => { const { attempt } = d; return attempt ? `Reconnection attempt ${attempt} failed` : 'Reconnection attempt failed'; }, 'online event reconnect failed': () => 'Could not reconnect when network came online', 'initial reconnect attempt failed': () => 'Initial reconnection failed', // Security and encryption 'decryption failed': () => 'Could not decrypt saved data - may need to re-enter API keys', 'failed to retrieve secure value': () => 'Could not load secure setting', 'secure storage migration failed': () => 'Secure storage needs recovery - some settings may be lost', 'secure storage migrated': () => 'Secure storage upgraded successfully', 'secure storage key mismatch': () => 'Secure storage needs recovery', 'failed to decrypt api keys': () => 'Could not load saved API keys - please re-enter them', 'failed to decrypt ai config': () => 'Could not load AI configuration - please re-enter API keys', 'secure storage write failed': () => 'Could not save secure setting', // Storage operations 'storage write failed': () => 'Could not save setting', 'storage remove failed': () => 'Could not clear setting', 'storage clear by prefix': () => 'Could not clear settings group', 'storage save failed': () => 'Settings saved to memory only (disk save failed)', 'failed to clear local preferences': () => 'Could not clear local settings cache', // Data loading 'failed to load meeting details': () => 'Could not load meeting details', 'failed to load recent meetings': () => 'Could not load recent meetings', 'failed to fetch meeting list': () => 'Could not load meetings list', 'failed to load tasks': () => 'Could not load tasks from meetings', // Notifications and reminders 'notification permission request failed': () => 'Could not enable notifications', 'notification creation failed': () => 'Desktop notification failed - showing in-app message', // Integration secrets 'secret retrieval failed': () => 'Could not load integration credentials', 'secret storage failed': () => 'Could not save integration credentials', 'secret clearing failed': () => 'Could not remove integration credentials', // Cache operations 'cache event listener error': () => 'Cache update error', 'meeting cache write failed': () => 'Could not cache meeting data', // Sync operations 'failed to persist preferences sync': () => 'Could not save settings to disk', 'failed to decode preference key': () => 'Settings key format error', 'validation event listener error': () => 'Settings validation error', // Export 'tauri path apis unavailable': () => 'Using default download location', // App policy 'recording blocked': () => 'Recording paused - blocked by app policy', // Additional backend messages 'meeting_created': () => 'New meeting has been created in the database', 'meeting_updated': () => 'Meeting information has been updated', 'meeting_deleted': () => 'Meeting has been removed from the database', 'preference_created': () => 'New user preference has been saved', 'preference_updated': () => 'User preference has been updated', 'preference_deleted': () => 'User preference has been removed', 'webhook_delivery_failed': () => 'Webhook notification delivery failed', 'calendar_list_events_failed': () => 'Unable to retrieve calendar events', 'oauth_disconnect_success': () => 'Successfully disconnected from external service', 'oauth_disconnect_failed': () => 'Failed to disconnect from external service', 'diarization_offline_model_loaded': () => 'Speaker identification model has been loaded', 'streaming_turns_cleared': () => 'Speaker turn data has been cleared', 'asr_config_requested_but_no_service': () => 'Audio configuration requested but service is unavailable', // Database and infrastructure 'running database migrations': () => 'Updating database...', 'project root discovered': () => 'Found project configuration', 'alembic version table exists': () => 'Checking database for updates', 'schema check': (d) => { const tableCount = d['table count']; const versionExists = d['alembic version exists']; if (tableCount && versionExists !== undefined) { return `Database ready (${tableCount} tables)`; } return 'Database schema check complete'; }, 'database connection pool ready': () => 'Database connected', 'alembic version table exists, checking if migrations needed': () => 'Checking database for updates', 'running migrations': () => 'Updating database...', 'applying migrations': () => 'Applying database changes...', 'migration complete': () => 'Database updated successfully', 'no migrations needed': () => 'Database is up to date', 'database initialization complete': () => 'Database setup complete', 'connection established': () => 'Connected to database', 'connection closed': () => 'Database connection closed', 'session started': () => 'Session started', 'session closed': () => 'Session ended', 'server initializing': () => 'Starting up...', 'server ready': () => 'Server is ready', 'server shutting down': () => 'Server is shutting down', }; const MESSAGE_ENTRIES = Object.entries(MESSAGE_TEMPLATES).map(([key, transformer]) => ({ key, keyLower: key.toLowerCase(), transformer, })); /** * Transform a technical log message into a friendly, human-readable version. * * @param message - The original technical log message * @param details - Key-value details from the log entry * @returns A human-readable message, or the original if no transformation applies */ export function toFriendlyMessage(message: string, details: Record): string { const lowerMessage = message.toLowerCase(); // Find matching template by checking if message starts with any known key for (const entry of MESSAGE_ENTRIES) { if (lowerMessage.startsWith(entry.keyLower)) { return entry.transformer(details); } } // No match found, return original message with basic cleanup return cleanupTechnicalMessage(message); } /** * Clean up a technical message for display when no template matches. * Applies formatting improvements to make messages more human-readable. */ function cleanupTechnicalMessage(message: string): string { let cleaned = message; // Remove [Namespace] prefixes from debug() function output // e.g., "[Preferences:Replace] Replacing preferences" → "Replacing preferences" cleaned = cleaned.replace(/^\[[^\]]+\]\s*/, ''); // Remove Namespace: prefixes // e.g., "Preferences:Replace: message" → "message" cleaned = cleaned.replace(/^[A-Za-z]+:[A-Za-z]+:\s*/, ''); // Remove common technical prefixes cleaned = cleaned .replace(/^RPC\s+/i, '') .replace(/^gRPC\s+/i, '') .replace(/^API\s+/i, '') .replace(/^Tauri\s+/i, '') .replace(/^TauriAdapter\./i, ''); // Convert method-style prefixes to readable text // e.g., "getPreferences: received" → "Preferences received" cleaned = cleaned.replace( /^(get|set|load|save|fetch|create|delete|update)([A-Z][a-zA-Z]*):\s*/i, (_: string, _verb: string, noun: string) => { const readableNoun = noun.replace(/([A-Z])/g, ' $1').trim(); return `${readableNoun} `; } ); // Convert "ENTRY" and similar debug markers to more readable text cleaned = cleaned.replace(/:\s*ENTRY$/i, ' started'); cleaned = cleaned.replace(/:\s*EXIT$/i, ' completed'); // Remove file extension references // e.g., "use-audio-devices.ts MODULE LOADED" → "Audio devices module loaded" cleaned = cleaned.replace(/[a-z-]+\.tsx?\s+(MODULE\s+)?LOADED/i, 'Module initialized'); // Convert snake_case segments to readable text // Only for isolated snake_case words, not in the middle of sentences cleaned = cleaned.replace(/\b([a-z]+)_([a-z]+)(?:_([a-z]+))?\b/gi, (_match: string, p1: string, p2: string, p3: string | undefined) => { const parts = [p1, p2, p3].filter((p): p is string => !!p); return parts.map((p: string) => p.charAt(0).toUpperCase() + p.slice(1).toLowerCase()).join(' '); }); // Clean up double spaces cleaned = cleaned.replace(/\s+/g, ' ').trim(); // Capitalize first letter if it's lowercase if (cleaned.length > 0 && cleaned[0] === cleaned[0].toLowerCase()) { cleaned = cleaned[0].toUpperCase() + cleaned.slice(1); } return cleaned; } /** * Check if a message is likely technical and would benefit from the technical view. * * @param message - The log message to check * @returns True if the message appears technical */ export function isTechnicalMessage(message: string): boolean { const technicalPatterns = [ /^[A-Z][a-z]+[A-Z]/, // CamelCase /^[a-z]+_[a-z]+/, // snake_case /\b(?:id|uuid|rpc|grpc|api|http|tcp|udp)\b/i, /\b[0-9a-f]{8,}/i, // Long hex strings (UUIDs, hashes) /:\s*\d+\s*$/, // Ends with port number /\b(?:status|code|err|error)=\w+/i, // Key=value pairs ]; return technicalPatterns.some((pattern) => pattern.test(message)); } /** * Get a summary description for a group of similar log messages. * * @param message - The common message pattern * @param count - Number of occurrences * @param details - Details from the first occurrence * @returns A summary string describing the group */ export function getSummaryMessage( message: string, count: number, details: Record ): string { const lowerMessage = message.toLowerCase(); // Segment processing is common and should be summarized specially if (lowerMessage.includes('segment')) { return `Processed ${count} transcript segments`; } // Webhook deliveries if (lowerMessage.includes('webhook')) { return `${count} webhook deliveries`; } // Generic summary const friendlyMessage = toFriendlyMessage(message, details); return `${friendlyMessage} (${count}x)`; }