440 lines
17 KiB
TypeScript
440 lines
17 KiB
TypeScript
/**
|
|
* 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, string>) => string;
|
|
|
|
/**
|
|
* Message templates for known log patterns.
|
|
* Keys are matched against the start of log messages (case-insensitive).
|
|
*/
|
|
const MESSAGE_TEMPLATES: Record<string, MessageTransformer> = {
|
|
// 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, string>): 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, string>
|
|
): 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)`;
|
|
}
|