diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 176acd2..ca5f0bf 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -1,6 +1,9 @@ 'use client' import { Component, type ErrorInfo, type ReactNode } from 'react' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('ErrorBoundary') interface Props { children: ReactNode @@ -23,7 +26,7 @@ export class ErrorBoundary extends Component { } componentDidCatch(error: Error, errorInfo: ErrorInfo) { - console.error('Panel error:', error, errorInfo) + log.error('Panel error:', error, errorInfo) } render() { diff --git a/src/components/chat/chat-panel.tsx b/src/components/chat/chat-panel.tsx index 0b23942..3584c3b 100644 --- a/src/components/chat/chat-panel.tsx +++ b/src/components/chat/chat-panel.tsx @@ -3,10 +3,13 @@ import { useEffect, useCallback, useState, useRef } from 'react' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' +import { createClientLogger } from '@/lib/client-logger' import { ConversationList } from './conversation-list' import { MessageList } from './message-list' import { ChatInput } from './chat-input' +const log = createClientLogger('ChatPanel') + export function ChatPanel() { const { chatPanelOpen, @@ -52,7 +55,7 @@ export function ChatPanel() { const data = await res.json() if (data.agents) setAgents(data.agents) } catch (err) { - console.error('Failed to load agents:', err) + log.error('Failed to load agents:', err) } } if (chatPanelOpen) loadAgents() @@ -67,7 +70,7 @@ export function ChatPanel() { const data = await res.json() if (data.messages) setChatMessages(data.messages) } catch (err) { - console.error('Failed to load messages:', err) + log.error('Failed to load messages:', err) } }, [activeConversation, setChatMessages]) @@ -145,7 +148,7 @@ export function ChatPanel() { updatePendingMessage(tempId, { pendingStatus: 'failed' }) } } catch (err) { - console.error('Failed to send message:', err) + log.error('Failed to send message:', err) updatePendingMessage(tempId, { pendingStatus: 'failed' }) } } diff --git a/src/components/chat/conversation-list.tsx b/src/components/chat/conversation-list.tsx index ee55a83..5a731d0 100644 --- a/src/components/chat/conversation-list.tsx +++ b/src/components/chat/conversation-list.tsx @@ -3,6 +3,9 @@ import { useState, useCallback } from 'react' import { useMissionControl, Conversation, Agent } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('ConversationList') function timeAgo(timestamp: number): string { const diff = Math.floor(Date.now() / 1000) - timestamp @@ -64,7 +67,7 @@ export function ConversationList({ onNewConversation }: ConversationListProps) { ) } } catch (err) { - console.error('Failed to load conversations:', err) + log.error('Failed to load conversations:', err) } }, [setConversations]) diff --git a/src/components/dashboard/sidebar.tsx b/src/components/dashboard/sidebar.tsx index 756caa2..1b07d72 100644 --- a/src/components/dashboard/sidebar.tsx +++ b/src/components/dashboard/sidebar.tsx @@ -3,6 +3,9 @@ import { useEffect, useState } from 'react' import { useMissionControl } from '@/store' import { useNavigateToPanel } from '@/lib/navigation' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('Sidebar') interface MenuItem { id: string @@ -36,7 +39,7 @@ export function Sidebar() { fetch('/api/status?action=overview') .then(res => res.json()) .then(data => setSystemStats(data)) - .catch(err => console.error('Failed to fetch system status:', err)) + .catch(err => log.error('Failed to fetch system status:', err)) }, []) const activeSessions = sessions.filter(s => s.active).length diff --git a/src/components/panels/agent-cost-panel.tsx b/src/components/panels/agent-cost-panel.tsx index fd34b75..9a9e559 100644 --- a/src/components/panels/agent-cost-panel.tsx +++ b/src/components/panels/agent-cost-panel.tsx @@ -1,11 +1,14 @@ 'use client' import { useState, useEffect, useCallback } from 'react' +import { createClientLogger } from '@/lib/client-logger' import { PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, } from 'recharts' +const log = createClientLogger('AgentCostPanel') + interface AgentCostData { stats: { totalTokens: number; totalCost: number; requestCount: number; avgTokensPerRequest: number; avgCostPerRequest: number } models: Record @@ -34,7 +37,7 @@ export function AgentCostPanel() { const json = await res.json() setData(json) } catch (err) { - console.error('Failed to load agent costs:', err) + log.error('Failed to load agent costs:', err) } finally { setIsLoading(false) } diff --git a/src/components/panels/agent-detail-tabs.tsx b/src/components/panels/agent-detail-tabs.tsx index ea42600..d975f50 100644 --- a/src/components/panels/agent-detail-tabs.tsx +++ b/src/components/panels/agent-detail-tabs.tsx @@ -1,8 +1,11 @@ 'use client' import { useState, useEffect } from 'react' +import { createClientLogger } from '@/lib/client-logger' +const log = createClientLogger('AgentDetailTabs') import Link from 'next/link' + interface Agent { id: number name: string @@ -374,7 +377,7 @@ export function SoulTab({ setSelectedTemplate(templateName) } } catch (error) { - console.error('Failed to load template:', error) + log.error('Failed to load template:', error) } } @@ -626,7 +629,7 @@ export function TasksTab({ agent }: { agent: Agent }) { setTasks(data.tasks || []) } } catch (error) { - console.error('Failed to fetch tasks:', error) + log.error('Failed to fetch tasks:', error) } finally { setLoading(false) } @@ -722,7 +725,7 @@ export function ActivityTab({ agent }: { agent: Agent }) { setActivities(data.activities || []) } } catch (error) { - console.error('Failed to fetch activities:', error) + log.error('Failed to fetch activities:', error) } finally { setLoading(false) } diff --git a/src/components/panels/agent-spawn-panel.tsx b/src/components/panels/agent-spawn-panel.tsx index dc8822a..24409ca 100644 --- a/src/components/panels/agent-spawn-panel.tsx +++ b/src/components/panels/agent-spawn-panel.tsx @@ -2,6 +2,9 @@ import { useState, useEffect } from 'react' import { useMissionControl } from '@/store' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('AgentSpawnPanel') interface SpawnFormData { task: string @@ -33,7 +36,7 @@ export function AgentSpawnPanel() { fetch('/api/spawn') .then(res => res.json()) .then(data => setSpawnHistory(data.history || [])) - .catch(err => console.error('Failed to load spawn history:', err)) + .catch(err => log.error('Failed to load spawn history:', err)) }, []) const handleSpawn = async () => { @@ -95,7 +98,7 @@ export function AgentSpawnPanel() { }) } } catch (error) { - console.error('Spawn error:', error) + log.error('Spawn error:', error) updateSpawnRequest(spawnId, { status: 'failed', error: error instanceof Error ? error.message : 'Network error' diff --git a/src/components/panels/agent-squad-panel-phase3.tsx b/src/components/panels/agent-squad-panel-phase3.tsx index f3f03a9..8905d31 100644 --- a/src/components/panels/agent-squad-panel-phase3.tsx +++ b/src/components/panels/agent-squad-panel-phase3.tsx @@ -2,6 +2,7 @@ import { useState, useEffect, useCallback } from 'react' import { useSmartPoll } from '@/lib/use-smart-poll' +import { createClientLogger } from '@/lib/client-logger' import { AgentAvatar } from '@/components/ui/agent-avatar' import { OverviewTab, @@ -13,6 +14,8 @@ import { CreateAgentModal } from './agent-detail-tabs' +const log = createClientLogger('AgentSquadPhase3') + interface Agent { id: number name: string @@ -148,7 +151,7 @@ export function AgentSquadPanelPhase3() { : agent )) } catch (error) { - console.error('Failed to update agent status:', error) + log.error('Failed to update agent status:', error) setError('Failed to update agent status') } } @@ -171,7 +174,7 @@ export function AgentSquadPanelPhase3() { await updateAgentStatus(agentName, 'idle', 'Manually woken via session') } catch (error) { - console.error('Failed to wake agent:', error) + log.error('Failed to wake agent:', error) setError('Failed to wake agent') } } @@ -503,7 +506,7 @@ function AgentDetailModalPhase3({ setSoulTemplates(data.templates || []) } } catch (error) { - console.error('Failed to load SOUL templates:', error) + log.error('Failed to load SOUL templates:', error) } } @@ -522,7 +525,7 @@ function AgentDetailModalPhase3({ setHeartbeatData(data) } } catch (error) { - console.error('Failed to perform heartbeat:', error) + log.error('Failed to perform heartbeat:', error) } finally { setLoadingHeartbeat(false) } @@ -544,7 +547,7 @@ function AgentDetailModalPhase3({ setEditing(false) onUpdate() } catch (error) { - console.error('Failed to update agent:', error) + log.error('Failed to update agent:', error) } } @@ -564,7 +567,7 @@ function AgentDetailModalPhase3({ setFormData(prev => ({ ...prev, soul_content: content })) onUpdate() } catch (error) { - console.error('Failed to update SOUL:', error) + log.error('Failed to update SOUL:', error) } } @@ -585,7 +588,7 @@ function AgentDetailModalPhase3({ setFormData(prev => ({ ...prev, working_memory: data.working_memory })) onUpdate() } catch (error) { - console.error('Failed to update memory:', error) + log.error('Failed to update memory:', error) } } @@ -746,7 +749,7 @@ function QuickSpawnModal({ alert(result.error || 'Failed to spawn agent') } } catch (error) { - console.error('Spawn failed:', error) + log.error('Spawn failed:', error) alert('Network error occurred') } finally { setIsSpawning(false) diff --git a/src/components/panels/agent-squad-panel.tsx b/src/components/panels/agent-squad-panel.tsx index f71ba29..a01439a 100644 --- a/src/components/panels/agent-squad-panel.tsx +++ b/src/components/panels/agent-squad-panel.tsx @@ -1,6 +1,9 @@ 'use client' import { useState, useEffect, useCallback } from 'react' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('AgentSquadPanel') interface Agent { id: number @@ -103,7 +106,7 @@ export function AgentSquadPanel() { : agent )) } catch (error) { - console.error('Failed to update agent status:', error) + log.error('Failed to update agent status:', error) setError('Failed to update agent status') } } @@ -355,7 +358,7 @@ function AgentDetailModal({ setEditing(false) onUpdate() } catch (error) { - console.error('Failed to update agent:', error) + log.error('Failed to update agent:', error) } } @@ -540,7 +543,7 @@ function CreateAgentModal({ onCreated() onClose() } catch (error) { - console.error('Error creating agent:', error) + log.error('Error creating agent:', error) } } diff --git a/src/components/panels/cron-management-panel.tsx b/src/components/panels/cron-management-panel.tsx index c5de4e6..7090834 100644 --- a/src/components/panels/cron-management-panel.tsx +++ b/src/components/panels/cron-management-panel.tsx @@ -2,6 +2,8 @@ import { useState, useEffect, useCallback, useMemo } from 'react' import { useMissionControl, CronJob } from '@/store' +import { createClientLogger } from '@/lib/client-logger' +const log = createClientLogger('CronManagement') import { buildDayKey, getCronOccurrences } from '@/lib/cron-occurrences' interface NewJobForm { @@ -92,7 +94,7 @@ export function CronManagementPanel() { const data = await response.json() setCronJobs(data.jobs || []) } catch (error) { - console.error('Failed to load cron jobs:', error) + log.error('Failed to load cron jobs:', error) } finally { setIsLoading(false) } @@ -126,7 +128,7 @@ export function CronManagementPanel() { const data = await response.json() setJobLogs(data.logs || []) } catch (error) { - console.error('Failed to load job logs:', error) + log.error('Failed to load job logs:', error) setJobLogs([]) } } @@ -150,7 +152,7 @@ export function CronManagementPanel() { alert(`Failed to toggle job: ${error.error}`) } } catch (error) { - console.error('Failed to toggle job:', error) + log.error('Failed to toggle job:', error) alert('Network error occurred') } } @@ -175,7 +177,7 @@ export function CronManagementPanel() { alert(`Job failed:\n${result.error}\n${result.stderr}`) } } catch (error) { - console.error('Failed to trigger job:', error) + log.error('Failed to trigger job:', error) alert('Network error occurred') } } @@ -214,7 +216,7 @@ export function CronManagementPanel() { alert(`Failed to add job: ${error.error}`) } } catch (error) { - console.error('Failed to add job:', error) + log.error('Failed to add job:', error) alert('Network error occurred') } } @@ -244,7 +246,7 @@ export function CronManagementPanel() { alert(`Failed to remove job: ${error.error}`) } } catch (error) { - console.error('Failed to remove job:', error) + log.error('Failed to remove job:', error) alert('Network error occurred') } } diff --git a/src/components/panels/log-viewer-panel.tsx b/src/components/panels/log-viewer-panel.tsx index 3f76c11..300b2a9 100644 --- a/src/components/panels/log-viewer-panel.tsx +++ b/src/components/panels/log-viewer-panel.tsx @@ -3,6 +3,9 @@ import { useState, useEffect, useRef, useCallback } from 'react' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('LogViewer') interface LogFilters { level?: string @@ -36,7 +39,7 @@ export function LogViewerPanel() { }, [logFilters]) const loadLogs = useCallback(async (tail = false) => { - console.log(`LogViewer: Loading logs (tail=${tail})`) + log.debug(`Loading logs (tail=${tail})`) setIsLoading(!tail) // Only show loading for initial load, not for tailing try { @@ -53,37 +56,37 @@ export function LogViewerPanel() { ...(tail && currentLogs.length > 0 && { since: currentLogs[0]?.timestamp.toString() }) }) - console.log(`LogViewer: Fetching /api/logs?${params}`) + log.debug(`Fetching /api/logs?${params}`) const response = await fetch(`/api/logs?${params}`) const data = await response.json() - console.log(`LogViewer: Received ${data.logs?.length || 0} logs from API`) + log.debug(`Received ${data.logs?.length || 0} logs from API`) if (data.logs && data.logs.length > 0) { if (tail) { // Add new logs for tail mode - prepend to existing logs let newLogsAdded = 0 const existingIds = new Set((currentLogs || []).map((l: any) => l?.id).filter(Boolean)) - data.logs.reverse().forEach((log: any) => { - if (existingIds.has(log?.id)) return - addLog(log) + data.logs.reverse().forEach((entry: any) => { + if (existingIds.has(entry?.id)) return + addLog(entry) newLogsAdded++ }) - console.log(`LogViewer: Added ${newLogsAdded} new logs (tail mode)`) + log.debug(`Added ${newLogsAdded} new logs (tail mode)`) } else { // Replace logs for initial load or refresh - console.log(`LogViewer: Clearing existing logs and loading ${data.logs.length} logs`) + log.debug(`Clearing existing logs and loading ${data.logs.length} logs`) clearLogs() // Clear existing logs - data.logs.reverse().forEach((log: any) => { - addLog(log) + data.logs.reverse().forEach((entry: any) => { + addLog(entry) }) - console.log(`LogViewer: Successfully added ${data.logs.length} logs to store`) + log.debug(`Successfully added ${data.logs.length} logs to store`) } } else { - console.log('LogViewer: No logs received from API') + log.debug('No logs received from API') } } catch (error) { - console.error('LogViewer: Failed to load logs:', error) + log.error('Failed to load logs:', error) } finally { setIsLoading(false) } @@ -95,13 +98,13 @@ export function LogViewerPanel() { const data = await response.json() setAvailableSources(data.sources || []) } catch (error) { - console.error('Failed to load log sources:', error) + log.error('Failed to load log sources:', error) } }, []) // Load initial logs and sources useEffect(() => { - console.log('LogViewer: Initial load started') + log.debug('Initial load started') loadLogs() loadSources() }, [loadLogs, loadSources]) @@ -154,16 +157,16 @@ export function LogViewerPanel() { } } - const filteredLogs = logs.filter(log => { - if (logFilters.level && log.level !== logFilters.level) return false - if (logFilters.source && log.source !== logFilters.source) return false - if (logFilters.search && !log.message.toLowerCase().includes(logFilters.search.toLowerCase())) return false - if (logFilters.session && (!log.session || !log.session.includes(logFilters.session))) return false + const filteredLogs = logs.filter(entry => { + if (logFilters.level && entry.level !== logFilters.level) return false + if (logFilters.source && entry.source !== logFilters.source) return false + if (logFilters.search && !entry.message.toLowerCase().includes(logFilters.search.toLowerCase())) return false + if (logFilters.session && (!entry.session || !entry.session.includes(logFilters.session))) return false return true }) // Debug logging - console.log(`LogViewer: Store has ${logs.length} logs, filtered to ${filteredLogs.length}`) + log.debug(`Store has ${logs.length} logs, filtered to ${filteredLogs.length}`) return (
diff --git a/src/components/panels/memory-browser-panel.tsx b/src/components/panels/memory-browser-panel.tsx index c393a64..61c9229 100644 --- a/src/components/panels/memory-browser-panel.tsx +++ b/src/components/panels/memory-browser-panel.tsx @@ -2,6 +2,9 @@ import React, { useState, useEffect, useCallback } from 'react' import { useMissionControl } from '@/store' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('MemoryBrowser') interface MemoryFile { path: string @@ -46,7 +49,7 @@ export function MemoryBrowserPanel() { // Auto-expand some common directories setExpandedFolders(new Set(['daily', 'knowledge'])) } catch (error) { - console.error('Failed to load file tree:', error) + log.error('Failed to load file tree:', error) } finally { setIsLoading(false) } @@ -83,7 +86,7 @@ export function MemoryBrowserPanel() { alert(data.error || 'Failed to load file content') } } catch (error) { - console.error('Failed to load file content:', error) + log.error('Failed to load file content:', error) alert('Network error occurred') } finally { setIsLoading(false) @@ -99,7 +102,7 @@ export function MemoryBrowserPanel() { const data = await response.json() setSearchResults(data.results || []) } catch (error) { - console.error('Search failed:', error) + log.error('Search failed:', error) setSearchResults([]) } finally { setIsSearching(false) @@ -165,7 +168,7 @@ export function MemoryBrowserPanel() { alert(data.error || 'Failed to save file') } } catch (error) { - console.error('Failed to save file:', error) + log.error('Failed to save file:', error) alert('Network error occurred') } finally { setIsSaving(false) @@ -192,7 +195,7 @@ export function MemoryBrowserPanel() { alert(data.error || 'Failed to create file') } } catch (error) { - console.error('Failed to create file:', error) + log.error('Failed to create file:', error) alert('Network error occurred') } } @@ -220,7 +223,7 @@ export function MemoryBrowserPanel() { alert(data.error || 'Failed to delete file') } } catch (error) { - console.error('Failed to delete file:', error) + log.error('Failed to delete file:', error) alert('Network error occurred') } } diff --git a/src/components/panels/session-details-panel.tsx b/src/components/panels/session-details-panel.tsx index be9ecac..c41c73a 100644 --- a/src/components/panels/session-details-panel.tsx +++ b/src/components/panels/session-details-panel.tsx @@ -3,6 +3,9 @@ import { useState, useCallback } from 'react' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('SessionDetails') export function SessionDetailsPanel() { const { @@ -20,7 +23,7 @@ export function SessionDetailsPanel() { const data = await response.json() setSessions(data.sessions || data) } catch (error) { - console.error('Failed to load sessions:', error) + log.error('Failed to load sessions:', error) } }, [setSessions]) diff --git a/src/components/panels/standup-panel.tsx b/src/components/panels/standup-panel.tsx index 0e707ed..2d2e044 100644 --- a/src/components/panels/standup-panel.tsx +++ b/src/components/panels/standup-panel.tsx @@ -1,6 +1,9 @@ 'use client' import { useState, useEffect } from 'react' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('StandupPanel') interface StandupReport { date: string @@ -133,7 +136,7 @@ export function StandupPanel() { const data = await response.json() setStandupHistory(data.history || []) } catch (err) { - console.error('Failed to fetch standup history:', err) + log.error('Failed to fetch standup history:', err) } } diff --git a/src/components/panels/task-board-panel.tsx b/src/components/panels/task-board-panel.tsx index 1dda867..22651b2 100644 --- a/src/components/panels/task-board-panel.tsx +++ b/src/components/panels/task-board-panel.tsx @@ -4,10 +4,16 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' + +import { createClientLogger } from '@/lib/client-logger' + import { useFocusTrap } from '@/lib/use-focus-trap' + import { AgentAvatar } from '@/components/ui/agent-avatar' import { MarkdownRenderer } from '@/components/markdown-renderer' +const log = createClientLogger('TaskBoard') + interface Task { id: number title: string @@ -937,7 +943,7 @@ function CreateTaskModal({ onCreated() onClose() } catch (error) { - console.error('Error creating task:', error) + log.error('Error creating task:', error) } } @@ -1086,7 +1092,7 @@ function EditTaskModal({ onUpdated() } catch (error) { - console.error('Error updating task:', error) + log.error('Error updating task:', error) } } diff --git a/src/components/panels/token-dashboard-panel.tsx b/src/components/panels/token-dashboard-panel.tsx index 4f46c51..9e850f4 100644 --- a/src/components/panels/token-dashboard-panel.tsx +++ b/src/components/panels/token-dashboard-panel.tsx @@ -2,8 +2,11 @@ import { useState, useEffect, useCallback } from 'react' import { useMissionControl } from '@/store' +import { createClientLogger } from '@/lib/client-logger' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell } from 'recharts' +const log = createClientLogger('TokenDashboard') + interface UsageStats { summary: { totalTokens: number @@ -39,7 +42,7 @@ export function TokenDashboardPanel() { const data = await response.json() setUsageStats(data) } catch (error) { - console.error('Failed to load usage stats:', error) + log.error('Failed to load usage stats:', error) } finally { setIsLoading(false) } @@ -51,7 +54,7 @@ export function TokenDashboardPanel() { const data = await response.json() setTrendData(data) } catch (error) { - console.error('Failed to load trend data:', error) + log.error('Failed to load trend data:', error) } }, [selectedTimeframe]) @@ -80,7 +83,7 @@ export function TokenDashboardPanel() { window.URL.revokeObjectURL(url) document.body.removeChild(a) } catch (error) { - console.error('Export failed:', error) + log.error('Export failed:', error) alert('Export failed: ' + error) } finally { setIsExporting(false) diff --git a/src/lib/client-logger.ts b/src/lib/client-logger.ts new file mode 100644 index 0000000..b18c699 --- /dev/null +++ b/src/lib/client-logger.ts @@ -0,0 +1,68 @@ +/** + * Lightweight structured logger for client-side ('use client') components. + * + * Mirrors pino's API surface (info/warn/error/debug) so call sites are + * consistent with the server-side logger in src/lib/logger.ts. + * In production builds, debug and info messages are suppressed. + */ + +type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +const LOG_LEVELS: Record = { + debug: 10, + info: 20, + warn: 30, + error: 40, +} + +const minLevel: number = + process.env.NODE_ENV === 'production' ? LOG_LEVELS.warn : LOG_LEVELS.debug + +function shouldLog(level: LogLevel): boolean { + return LOG_LEVELS[level] >= minLevel +} + +function formatArgs( + level: LogLevel, + module: string, + msgOrObj: unknown, + ...rest: unknown[] +): unknown[] { + const prefix = `[${level.toUpperCase()}] ${module}:` + if (typeof msgOrObj === 'string') { + return [prefix, msgOrObj, ...rest] + } + return [prefix, msgOrObj, ...rest] +} + +export interface ClientLogger { + debug(msg: string, ...args: unknown[]): void + debug(obj: Record, msg?: string): void + info(msg: string, ...args: unknown[]): void + info(obj: Record, msg?: string): void + warn(msg: string, ...args: unknown[]): void + warn(obj: Record, msg?: string): void + error(msg: string, ...args: unknown[]): void + error(obj: Record, msg?: string): void +} + +export function createClientLogger(module: string): ClientLogger { + return { + debug(msgOrObj: unknown, ...rest: unknown[]) { + if (!shouldLog('debug')) return + console.debug(...formatArgs('debug', module, msgOrObj, ...rest)) + }, + info(msgOrObj: unknown, ...rest: unknown[]) { + if (!shouldLog('info')) return + console.info(...formatArgs('info', module, msgOrObj, ...rest)) + }, + warn(msgOrObj: unknown, ...rest: unknown[]) { + if (!shouldLog('warn')) return + console.warn(...formatArgs('warn', module, msgOrObj, ...rest)) + }, + error(msgOrObj: unknown, ...rest: unknown[]) { + if (!shouldLog('error')) return + console.error(...formatArgs('error', module, msgOrObj, ...rest)) + }, + } +} diff --git a/src/lib/device-identity.ts b/src/lib/device-identity.ts index e1d28e6..9a2cef0 100644 --- a/src/lib/device-identity.ts +++ b/src/lib/device-identity.ts @@ -1,5 +1,9 @@ 'use client' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('DeviceIdentity') + /** * Ed25519 device identity for OpenClaw gateway protocol v3 challenge-response. * @@ -101,7 +105,7 @@ export async function getOrCreateDeviceIdentity(): Promise { } } catch { // Stored key corrupted — regenerate - console.warn('Device identity keys corrupted, regenerating...') + log.warn('Device identity keys corrupted, regenerating...') } } diff --git a/src/lib/use-server-events.ts b/src/lib/use-server-events.ts index a0bbf7a..347c12f 100644 --- a/src/lib/use-server-events.ts +++ b/src/lib/use-server-events.ts @@ -2,6 +2,9 @@ import { useEffect, useRef } from 'react' import { useMissionControl } from '@/store' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('SSE') interface ServerEvent { type: string @@ -73,7 +76,7 @@ export function useServerEvents() { const attempts = sseReconnectAttemptsRef.current if (attempts >= SSE_MAX_RECONNECT_ATTEMPTS) { - console.error(`SSE: max reconnect attempts (${SSE_MAX_RECONNECT_ATTEMPTS}) reached`) + log.error(`Max reconnect attempts (${SSE_MAX_RECONNECT_ATTEMPTS}) reached`) return } @@ -82,7 +85,7 @@ export function useServerEvents() { const delay = Math.round(base + Math.random() * base * 0.5) sseReconnectAttemptsRef.current = attempts + 1 - console.warn(`SSE: reconnecting in ${delay}ms (attempt ${attempts + 1}/${SSE_MAX_RECONNECT_ATTEMPTS})`) + log.warn(`Reconnecting in ${delay}ms (attempt ${attempts + 1}/${SSE_MAX_RECONNECT_ATTEMPTS})`) reconnectTimeoutRef.current = setTimeout(() => { if (mounted) connect() }, delay) diff --git a/src/lib/websocket.ts b/src/lib/websocket.ts index 663de63..a1ab370 100644 --- a/src/lib/websocket.ts +++ b/src/lib/websocket.ts @@ -10,6 +10,9 @@ import { cacheDeviceToken, } from '@/lib/device-identity' import { APP_VERSION } from '@/lib/version' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('WebSocket') // Gateway protocol version (v3 required by OpenClaw 2026.x) const PROTOCOL_VERSION = 3 @@ -116,7 +119,7 @@ export function useWebSocket() { // Check missed pongs if (missedPongsRef.current >= MAX_MISSED_PONGS) { - console.warn(`Missed ${MAX_MISSED_PONGS} pongs, triggering reconnect`) + log.warn(`Missed ${MAX_MISSED_PONGS} pongs, triggering reconnect`) addLog({ id: `heartbeat-${Date.now()}`, timestamp: Date.now(), @@ -213,7 +216,7 @@ export function useWebSocket() { nonce, } } catch (err) { - console.warn('Device identity unavailable, proceeding without:', err) + log.warn('Device identity unavailable, proceeding without:', err) } } @@ -239,7 +242,7 @@ export function useWebSocket() { deviceToken: cachedToken || undefined, } } - console.log('Sending connect handshake:', connectRequest) + log.info('Sending connect handshake') ws.send(JSON.stringify(connectRequest)) }, []) @@ -249,7 +252,7 @@ export function useWebSocket() { // Debug logging for development if (process.env.NODE_ENV === 'development') { - console.log('WebSocket message received:', message.type, message) + log.debug(`Message received: ${message.type}`) } switch (message.type) { @@ -319,24 +322,24 @@ export function useWebSocket() { break default: - console.log('Unknown gateway message type:', message.type) + log.warn(`Unknown gateway message type: ${message.type}`) } }, [setLastMessage, setSessions, addLog, updateSpawnRequest, setCronJobs, addTokenUsage]) // Handle gateway protocol frames const handleGatewayFrame = useCallback((frame: GatewayFrame, ws: WebSocket) => { - console.log('Gateway frame:', frame) + log.debug(`Gateway frame: ${frame.type}`) // Handle connect challenge if (frame.type === 'event' && frame.event === 'connect.challenge') { - console.log('Received connect challenge, sending handshake...') + log.info('Received connect challenge, sending handshake') sendConnectHandshake(ws, frame.payload?.nonce) return } // Handle connect response (handshake success) if (frame.type === 'res' && frame.ok && !handshakeCompleteRef.current) { - console.log('Handshake complete!') + log.info('Handshake complete') handshakeCompleteRef.current = true reconnectAttemptsRef.current = 0 // Cache device token if returned by gateway @@ -361,7 +364,7 @@ export function useWebSocket() { // Handle connect error if (frame.type === 'res' && !frame.ok) { - console.error('Gateway error:', frame.error) + log.error(`Gateway error: ${frame.error?.message || JSON.stringify(frame.error)}`) const rawMessage = frame.error?.message || JSON.stringify(frame.error) const help = getGatewayErrorHelp(rawMessage) const nonRetryable = isNonRetryableGatewayError(rawMessage) @@ -510,14 +513,14 @@ export function useWebSocket() { wsRef.current = ws ws.onopen = () => { - console.log('WebSocket connected to', url.split('?')[0]) + log.info(`Connected to ${url.split('?')[0]}`) // Don't set isConnected yet - wait for handshake setConnection({ url: url.split('?')[0], reconnectAttempts: 0 }) // Wait for connect.challenge from server - console.log('Waiting for connect challenge...') + log.debug('Waiting for connect challenge') } ws.onmessage = (event) => { @@ -525,7 +528,7 @@ export function useWebSocket() { const frame = JSON.parse(event.data) as GatewayFrame handleGatewayFrame(frame, ws) } catch (error) { - console.error('Failed to parse WebSocket message:', error) + log.error('Failed to parse WebSocket message:', error) addLog({ id: `raw-${Date.now()}`, timestamp: Date.now(), @@ -537,7 +540,7 @@ export function useWebSocket() { } ws.onclose = (event) => { - console.log('Disconnected from Gateway:', event.code, event.reason) + log.info(`Disconnected from Gateway: ${event.code} ${event.reason}`) setConnection({ isConnected: false }) handshakeCompleteRef.current = false stopHeartbeat() @@ -555,7 +558,7 @@ export function useWebSocket() { if (attempts < maxReconnectAttempts) { const base = Math.min(Math.pow(2, attempts) * 1000, 30000) const timeout = Math.round(base + Math.random() * base * 0.5) - console.log(`Reconnecting in ${timeout}ms... (attempt ${attempts + 1}/${maxReconnectAttempts})`) + log.info(`Reconnecting in ${timeout}ms (attempt ${attempts + 1}/${maxReconnectAttempts})`) reconnectAttemptsRef.current = attempts + 1 setConnection({ reconnectAttempts: attempts + 1 }) @@ -563,7 +566,7 @@ export function useWebSocket() { connectRef.current(reconnectUrl.current, authTokenRef.current) }, timeout) } else { - console.error('Max reconnection attempts reached.') + log.error('Max reconnection attempts reached') addLog({ id: `error-${Date.now()}`, timestamp: Date.now(), @@ -575,7 +578,7 @@ export function useWebSocket() { } ws.onerror = (error) => { - console.error('WebSocket error:', error) + log.error('WebSocket error:', error) addLog({ id: `error-${Date.now()}`, timestamp: Date.now(), @@ -586,7 +589,7 @@ export function useWebSocket() { } } catch (error) { - console.error('Failed to connect to WebSocket:', error) + log.error('Failed to connect to WebSocket:', error) setConnection({ isConnected: false }) } }, [setConnection, handleGatewayFrame, addLog, stopHeartbeat])