/** * Centralized Logger for Root Org * * Works on both client and server. Outputs structured logs with: * - Timestamp * - Level (debug/info/warn/error) * - Context (which module/function) * - Structured data * * On the server (dev terminal), logs are colorized and always visible. * On the client, logs go to console and can optionally trigger toasts. */ export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; export interface LogEntry { level: LogLevel; context: string; message: string; data?: unknown; error?: unknown; timestamp: string; } const LEVEL_PRIORITY: Record = { debug: 0, info: 1, warn: 2, error: 3, }; const LEVEL_COLORS: Record = { debug: '\x1b[36m', // cyan info: '\x1b[32m', // green warn: '\x1b[33m', // yellow error: '\x1b[31m', // red }; const RESET = '\x1b[0m'; const BOLD = '\x1b[1m'; const DIM = '\x1b[2m'; // Minimum level to output — debug in dev, info in prod let minLevel: LogLevel = typeof window !== 'undefined' && window.location?.hostname === 'localhost' ? 'debug' : 'info'; function shouldLog(level: LogLevel): boolean { return LEVEL_PRIORITY[level] >= LEVEL_PRIORITY[minLevel]; } function isServer(): boolean { return typeof window === 'undefined'; } function formatError(err: unknown): string { if (err instanceof Error) { const stack = err.stack ? `\n${err.stack}` : ''; return `${err.name}: ${err.message}${stack}`; } if (typeof err === 'object' && err !== null) { try { return JSON.stringify(err, null, 2); } catch { return String(err); } } return String(err); } function formatData(data: unknown): string { if (data === undefined || data === null) return ''; try { return JSON.stringify(data, null, 2); } catch { return String(data); } } function serverLog(entry: LogEntry) { const color = LEVEL_COLORS[entry.level]; const levelTag = `${color}${BOLD}[${entry.level.toUpperCase()}]${RESET}`; const time = `${DIM}${entry.timestamp}${RESET}`; const ctx = `${color}[${entry.context}]${RESET}`; let line = `${levelTag} ${time} ${ctx} ${entry.message}`; if (entry.data !== undefined) { line += `\n ${DIM}data:${RESET} ${formatData(entry.data)}`; } if (entry.error !== undefined) { line += `\n ${color}error:${RESET} ${formatError(entry.error)}`; } // Use stderr for errors/warnings so they stand out in terminal if (entry.level === 'error') { console.error(line); } else if (entry.level === 'warn') { console.warn(line); } else { console.log(line); } } function clientLog(entry: LogEntry) { const prefix = `[${entry.level.toUpperCase()}] [${entry.context}]`; const args: unknown[] = [prefix, entry.message]; if (entry.data !== undefined) args.push(entry.data); if (entry.error !== undefined) args.push(entry.error); switch (entry.level) { case 'error': console.error(...args); break; case 'warn': console.warn(...args); break; case 'debug': console.debug(...args); break; default: console.log(...args); } } function log(level: LogLevel, context: string, message: string, extra?: { data?: unknown; error?: unknown }) { if (!shouldLog(level)) return; const entry: LogEntry = { level, context, message, data: extra?.data, error: extra?.error, timestamp: new Date().toISOString(), }; if (isServer()) { serverLog(entry); } else { clientLog(entry); } // Store in recent logs buffer for debugging recentLogs.push(entry); if (recentLogs.length > MAX_RECENT_LOGS) { recentLogs.shift(); } return entry; } // Ring buffer of recent logs — useful for dumping context on crash const MAX_RECENT_LOGS = 100; const recentLogs: LogEntry[] = []; /** * Create a scoped logger for a specific module/context. * * Usage: * ```ts * const log = createLogger('kanban.api'); * log.info('Loading board', { data: { boardId } }); * log.error('Failed to load board', { error: err, data: { boardId } }); * ``` */ export function createLogger(context: string) { return { debug: (message: string, extra?: { data?: unknown; error?: unknown }) => log('debug', context, message, extra), info: (message: string, extra?: { data?: unknown; error?: unknown }) => log('info', context, message, extra), warn: (message: string, extra?: { data?: unknown; error?: unknown }) => log('warn', context, message, extra), error: (message: string, extra?: { data?: unknown; error?: unknown }) => log('error', context, message, extra), }; } /** Set the minimum log level */ export function setLogLevel(level: LogLevel) { minLevel = level; } /** Get recent log entries (for error reports / debugging) */ export function getRecentLogs(): LogEntry[] { return [...recentLogs]; } /** Clear recent logs */ export function clearRecentLogs() { recentLogs.length = 0; } /** * Format recent logs as a copyable string for bug reports. * User can paste this to you for debugging. */ export function dumpLogs(): string { return recentLogs .map((e) => { let line = `[${e.level.toUpperCase()}] ${e.timestamp} [${e.context}] ${e.message}`; if (e.data !== undefined) line += ` | data: ${formatData(e.data)}`; if (e.error !== undefined) line += ` | error: ${formatError(e.error)}`; return line; }) .join('\n'); }