From 3347eadbf8de7e15570f2105bab0bd53d75b93a8 Mon Sep 17 00:00:00 2001 From: Pedram Amini Date: Sun, 23 Nov 2025 23:32:47 -0600 Subject: [PATCH] feat: implement structured logging system with UI controls Implements core logging infrastructure for task #15 (Housekeeping.md). This is a foundational implementation that provides the backend and settings UI. Full log viewer UI remains as future work. ## Changes ### Main Process - Created `src/main/utils/logger.ts` with full logging system - Supports debug, info, warn, error log levels - Stores last 1000 log entries in memory - Filters based on configured minimum log level - Outputs to console with timestamps and context ### IPC Layer - Added logger API to preload.ts with 5 operations: - `logger:log` - Send log entry from renderer - `logger:getLogs` - Retrieve filtered logs - `logger:clearLogs` - Clear log storage - `logger:setLogLevel` - Update minimum log level - `logger:getLogLevel` - Get current log level - Added IPC handlers in main/index.ts - Log level persists to electron-store (maestro-settings.json) ### Renderer Process - Created `src/renderer/utils/logger.ts` for renderer logging - Integrated with useSettings hook for persistence - Added Log Level selector to Settings > General tab - Color-coded pills: Debug (indigo), Info (blue), Warn (amber), Error (red) - Setting persists across app restarts ## Remaining Work (Task #15) - Create LogViewer component with search functionality - Integrate LogViewer with Command-K menu - Replace 33 console.log/error calls with new logger - Add color-coded log display in viewer - Implement `/` search in log viewer ## Testing - Build verified successful - No TypeScript errors - Log level setting integrated with electron-store Related: tmp/Housekeeping.md #15 --- src/main/index.ts | 49 ++++++++ src/main/preload.ts | 24 ++++ src/main/utils/logger.ts | 138 ++++++++++++++++++++++ src/renderer/components/SettingsModal.tsx | 60 ++++++++++ src/renderer/hooks/useSettings.ts | 16 +++ src/renderer/utils/logger.ts | 27 +++++ 6 files changed, 314 insertions(+) create mode 100644 src/main/utils/logger.ts create mode 100644 src/renderer/utils/logger.ts diff --git a/src/main/index.ts b/src/main/index.ts index 22efd599..7064f99a 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,6 +5,7 @@ import { ProcessManager } from './process-manager'; import { WebServer } from './web-server'; import { AgentDetector } from './agent-detector'; import { execFileNoThrow } from './utils/execFile'; +import { logger } from './utils/logger'; import Store from 'electron-store'; // Type definitions @@ -20,6 +21,7 @@ interface MaestroSettings { fontSize: number; fontFamily: string; customFonts: string[]; + logLevel: 'debug' | 'info' | 'warn' | 'error'; } const store = new Store({ @@ -36,6 +38,7 @@ const store = new Store({ fontSize: 14, fontFamily: 'Roboto Mono, Menlo, "Courier New", monospace', customFonts: [], + logLevel: 'info', }, }); @@ -106,6 +109,10 @@ app.whenReady().then(() => { webServer = new WebServer(8000); agentDetector = new AgentDetector(); + // Load logger settings + const logLevel = store.get('logLevel', 'info'); + logger.setLogLevel(logLevel); + // Set up IPC handlers setupIpcHandlers(); @@ -334,6 +341,48 @@ function setupIpcHandlers() { } } }); + + // Logger operations + ipcMain.handle('logger:log', async (_event, level: string, message: string, context?: string, data?: unknown) => { + const logLevel = level as 'debug' | 'info' | 'warn' | 'error'; + switch (logLevel) { + case 'debug': + logger.debug(message, context, data); + break; + case 'info': + logger.info(message, context, data); + break; + case 'warn': + logger.warn(message, context, data); + break; + case 'error': + logger.error(message, context, data); + break; + } + }); + + ipcMain.handle('logger:getLogs', async (_event, filter?: { level?: string; context?: string; limit?: number }) => { + const typedFilter = filter ? { + level: filter.level as 'debug' | 'info' | 'warn' | 'error' | undefined, + context: filter.context, + limit: filter.limit, + } : undefined; + return logger.getLogs(typedFilter); + }); + + ipcMain.handle('logger:clearLogs', async () => { + logger.clearLogs(); + }); + + ipcMain.handle('logger:setLogLevel', async (_event, level: string) => { + const logLevel = level as 'debug' | 'info' | 'warn' | 'error'; + logger.setLogLevel(logLevel); + store.set('logLevel', logLevel); + }); + + ipcMain.handle('logger:getLogLevel', async () => { + return logger.getLogLevel(); + }); } // Handle process output streaming (set up after initialization) diff --git a/src/main/preload.ts b/src/main/preload.ts index 0d80523f..4a6bb965 100644 --- a/src/main/preload.ts +++ b/src/main/preload.ts @@ -106,6 +106,17 @@ contextBridge.exposeInMainWorld('maestro', { close: () => ipcRenderer.invoke('devtools:close'), toggle: () => ipcRenderer.invoke('devtools:toggle'), }, + + // Logger API + logger: { + log: (level: string, message: string, context?: string, data?: unknown) => + ipcRenderer.invoke('logger:log', level, message, context, data), + getLogs: (filter?: { level?: string; context?: string; limit?: number }) => + ipcRenderer.invoke('logger:getLogs', filter), + clearLogs: () => ipcRenderer.invoke('logger:clearLogs'), + setLogLevel: (level: string) => ipcRenderer.invoke('logger:setLogLevel', level), + getLogLevel: () => ipcRenderer.invoke('logger:getLogLevel'), + }, }); // Type definitions for TypeScript @@ -161,6 +172,19 @@ export interface MaestroAPI { close: () => Promise; toggle: () => Promise; }; + logger: { + log: (level: string, message: string, context?: string, data?: unknown) => Promise; + getLogs: (filter?: { level?: string; context?: string; limit?: number }) => Promise>; + clearLogs: () => Promise; + setLogLevel: (level: string) => Promise; + getLogLevel: () => Promise; + }; } declare global { diff --git a/src/main/utils/logger.ts b/src/main/utils/logger.ts new file mode 100644 index 00000000..e7549a40 --- /dev/null +++ b/src/main/utils/logger.ts @@ -0,0 +1,138 @@ +/** + * Structured logging utility for the main process + * Logs are stored in memory and can be retrieved via IPC + */ + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +export interface LogEntry { + timestamp: number; + level: LogLevel; + message: string; + context?: string; + data?: unknown; +} + +class Logger { + private logs: LogEntry[] = []; + private maxLogs = 1000; // Keep last 1000 log entries + private minLevel: LogLevel = 'info'; // Default log level + + private levelPriority: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + }; + + setLogLevel(level: LogLevel): void { + this.minLevel = level; + } + + getLogLevel(): LogLevel { + return this.minLevel; + } + + private shouldLog(level: LogLevel): boolean { + return this.levelPriority[level] >= this.levelPriority[this.minLevel]; + } + + private addLog(entry: LogEntry): void { + this.logs.push(entry); + + // Keep only the last maxLogs entries + if (this.logs.length > this.maxLogs) { + this.logs = this.logs.slice(-this.maxLogs); + } + + // Also output to console for development + const timestamp = new Date(entry.timestamp).toISOString(); + const prefix = `[${timestamp}] [${entry.level.toUpperCase()}]${entry.context ? ` [${entry.context}]` : ''}`; + const message = `${prefix} ${entry.message}`; + + switch (entry.level) { + case 'error': + console.error(message, entry.data || ''); + break; + case 'warn': + console.warn(message, entry.data || ''); + break; + case 'info': + console.info(message, entry.data || ''); + break; + case 'debug': + console.log(message, entry.data || ''); + break; + } + } + + debug(message: string, context?: string, data?: unknown): void { + if (!this.shouldLog('debug')) return; + this.addLog({ + timestamp: Date.now(), + level: 'debug', + message, + context, + data, + }); + } + + info(message: string, context?: string, data?: unknown): void { + if (!this.shouldLog('info')) return; + this.addLog({ + timestamp: Date.now(), + level: 'info', + message, + context, + data, + }); + } + + warn(message: string, context?: string, data?: unknown): void { + if (!this.shouldLog('warn')) return; + this.addLog({ + timestamp: Date.now(), + level: 'warn', + message, + context, + data, + }); + } + + error(message: string, context?: string, data?: unknown): void { + if (!this.shouldLog('error')) return; + this.addLog({ + timestamp: Date.now(), + level: 'error', + message, + context, + data, + }); + } + + getLogs(filter?: { level?: LogLevel; context?: string; limit?: number }): LogEntry[] { + let filtered = [...this.logs]; + + if (filter?.level) { + const minPriority = this.levelPriority[filter.level]; + filtered = filtered.filter(log => this.levelPriority[log.level] >= minPriority); + } + + if (filter?.context) { + filtered = filtered.filter(log => log.context === filter.context); + } + + if (filter?.limit) { + filtered = filtered.slice(-filter.limit); + } + + return filtered; + } + + clearLogs(): void { + this.logs = []; + } +} + +// Export singleton instance +export const logger = new Logger(); diff --git a/src/renderer/components/SettingsModal.tsx b/src/renderer/components/SettingsModal.tsx index 84a10821..1313ef99 100644 --- a/src/renderer/components/SettingsModal.tsx +++ b/src/renderer/components/SettingsModal.tsx @@ -27,6 +27,8 @@ interface SettingsModalProps { setFontFamily: (font: string) => void; fontSize: number; setFontSize: (size: number) => void; + logLevel: string; + setLogLevel: (level: string) => void; initialTab?: 'general' | 'llm' | 'shortcuts' | 'theme' | 'network'; } @@ -596,6 +598,64 @@ export function SettingsModal(props: SettingsModalProps) { + + {/* Log Level */} +
+ +
+ + + + +
+

+ Higher levels show fewer logs. Debug shows all logs, Error shows only errors. +

+
)} diff --git a/src/renderer/hooks/useSettings.ts b/src/renderer/hooks/useSettings.ts index 826537e9..6e495721 100644 --- a/src/renderer/hooks/useSettings.ts +++ b/src/renderer/hooks/useSettings.ts @@ -41,6 +41,10 @@ export interface UseSettingsReturn { setRightPanelWidth: (value: number) => void; setMarkdownRawMode: (value: boolean) => void; + // Logging settings + logLevel: string; + setLogLevel: (value: string) => void; + // Shortcuts shortcuts: Record; setShortcuts: (value: Record) => void; @@ -71,6 +75,9 @@ export function useSettings(): UseSettingsReturn { const [rightPanelWidth, setRightPanelWidthState] = useState(384); const [markdownRawMode, setMarkdownRawModeState] = useState(false); + // Logging Config + const [logLevel, setLogLevelState] = useState('info'); + // Shortcuts const [shortcuts, setShortcutsState] = useState>(DEFAULT_SHORTCUTS); @@ -150,6 +157,11 @@ export function useSettings(): UseSettingsReturn { window.maestro.settings.set('shortcuts', value); }; + const setLogLevel = async (value: string) => { + setLogLevelState(value); + await window.maestro.logger.setLogLevel(value); + }; + // Load settings from electron-store on mount useEffect(() => { const loadSettings = async () => { @@ -168,6 +180,7 @@ export function useSettings(): UseSettingsReturn { const savedMarkdownRawMode = await window.maestro.settings.get('markdownRawMode'); const savedShortcuts = await window.maestro.settings.get('shortcuts'); const savedActiveThemeId = await window.maestro.settings.get('activeThemeId'); + const savedLogLevel = await window.maestro.logger.getLogLevel(); if (savedEnterToSend !== undefined) setEnterToSendState(savedEnterToSend); if (savedLlmProvider !== undefined) setLlmProviderState(savedLlmProvider); @@ -183,6 +196,7 @@ export function useSettings(): UseSettingsReturn { if (savedRightPanelWidth !== undefined) setRightPanelWidthState(savedRightPanelWidth); if (savedMarkdownRawMode !== undefined) setMarkdownRawModeState(savedMarkdownRawMode); if (savedActiveThemeId !== undefined) setActiveThemeIdState(savedActiveThemeId); + if (savedLogLevel !== undefined) setLogLevelState(savedLogLevel); // Merge saved shortcuts with defaults (in case new shortcuts were added) if (savedShortcuts !== undefined) { @@ -226,6 +240,8 @@ export function useSettings(): UseSettingsReturn { setLeftSidebarWidth, setRightPanelWidth, setMarkdownRawMode, + logLevel, + setLogLevel, shortcuts, setShortcuts, }; diff --git a/src/renderer/utils/logger.ts b/src/renderer/utils/logger.ts new file mode 100644 index 00000000..d427ec44 --- /dev/null +++ b/src/renderer/utils/logger.ts @@ -0,0 +1,27 @@ +/** + * Structured logging utility for the renderer process + * Sends logs to the main process via IPC + */ + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +class RendererLogger { + debug(message: string, context?: string, data?: unknown): void { + window.maestro?.logger?.log('debug', message, context, data); + } + + info(message: string, context?: string, data?: unknown): void { + window.maestro?.logger?.log('info', message, context, data); + } + + warn(message: string, context?: string, data?: unknown): void { + window.maestro?.logger?.log('warn', message, context, data); + } + + error(message: string, context?: string, data?: unknown): void { + window.maestro?.logger?.log('error', message, context, data); + } +} + +// Export singleton instance +export const logger = new RendererLogger();