From 797799ae0bcdefbcebfbe626cdbe2274aa6f7130 Mon Sep 17 00:00:00 2001 From: Bhavikprit Date: Wed, 4 Mar 2026 11:29:48 +0400 Subject: [PATCH 1/3] feat(#39): replace console.log/error/warn with structured client logger Add src/lib/client-logger.ts and replace all 55+ console calls across 19 client files with scoped createClientLogger instances. Production suppresses debug+info. Fix variable shadowing in log-viewer-panel.tsx. Addresses #39 --- src/components/ErrorBoundary.tsx | 5 +- src/components/chat/chat-panel.tsx | 9 ++- src/components/chat/conversation-list.tsx | 5 +- src/components/dashboard/sidebar.tsx | 5 +- src/components/panels/agent-cost-panel.tsx | 5 +- src/components/panels/agent-detail-tabs.tsx | 9 ++- src/components/panels/agent-spawn-panel.tsx | 7 +- .../panels/agent-squad-panel-phase3.tsx | 19 +++--- src/components/panels/agent-squad-panel.tsx | 9 ++- .../panels/cron-management-panel.tsx | 15 ++-- src/components/panels/log-viewer-panel.tsx | 45 ++++++------ .../panels/memory-browser-panel.tsx | 15 ++-- .../panels/session-details-panel.tsx | 5 +- src/components/panels/standup-panel.tsx | 5 +- src/components/panels/task-board-panel.tsx | 7 +- .../panels/token-dashboard-panel.tsx | 9 ++- src/lib/client-logger.ts | 68 +++++++++++++++++++ src/lib/device-identity.ts | 6 +- src/lib/use-server-events.ts | 7 +- src/lib/websocket.ts | 37 +++++----- 20 files changed, 209 insertions(+), 83 deletions(-) create mode 100644 src/lib/client-logger.ts 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 7f95ed2..d67ee19 100644 --- a/src/components/panels/agent-detail-tabs.tsx +++ b/src/components/panels/agent-detail-tabs.tsx @@ -1,6 +1,9 @@ 'use client' import { useState, useEffect } from 'react' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('AgentDetailTabs') interface Agent { id: number @@ -373,7 +376,7 @@ export function SoulTab({ setSelectedTemplate(templateName) } } catch (error) { - console.error('Failed to load template:', error) + log.error('Failed to load template:', error) } } @@ -620,7 +623,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) } @@ -713,7 +716,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 ec2bac1..4356a15 100644 --- a/src/components/panels/cron-management-panel.tsx +++ b/src/components/panels/cron-management-panel.tsx @@ -2,6 +2,9 @@ import { useState, useEffect, useCallback } from 'react' import { useMissionControl, CronJob } from '@/store' +import { createClientLogger } from '@/lib/client-logger' + +const log = createClientLogger('CronManagement') interface NewJobForm { name: string @@ -90,7 +93,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) } @@ -124,7 +127,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([]) } } @@ -148,7 +151,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') } } @@ -173,7 +176,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') } } @@ -212,7 +215,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') } } @@ -242,7 +245,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 7eeba6e..aac4db7 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 9aabc92..b5d12a6 100644 --- a/src/components/panels/task-board-panel.tsx +++ b/src/components/panels/task-board-panel.tsx @@ -3,9 +3,12 @@ import { useState, useEffect, useCallback, useRef } from 'react' import { useMissionControl } from '@/store' import { useSmartPoll } from '@/lib/use-smart-poll' +import { createClientLogger } from '@/lib/client-logger' import { AgentAvatar } from '@/components/ui/agent-avatar' import { MarkdownRenderer } from '@/components/markdown-renderer' +const log = createClientLogger('TaskBoard') + interface Task { id: number title: string @@ -869,7 +872,7 @@ function CreateTaskModal({ onCreated() onClose() } catch (error) { - console.error('Error creating task:', error) + log.error('Error creating task:', error) } } @@ -1011,7 +1014,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 efa3d74..a595f4b 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 @@ -115,7 +118,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(), @@ -212,7 +215,7 @@ export function useWebSocket() { nonce, } } catch (err) { - console.warn('Device identity unavailable, proceeding without:', err) + log.warn('Device identity unavailable, proceeding without:', err) } } @@ -238,7 +241,7 @@ export function useWebSocket() { deviceToken: cachedToken || undefined, } } - console.log('Sending connect handshake:', connectRequest) + log.info('Sending connect handshake') ws.send(JSON.stringify(connectRequest)) }, []) @@ -248,7 +251,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) { @@ -318,24 +321,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 @@ -360,7 +363,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) @@ -509,14 +512,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) => { @@ -524,7 +527,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(), @@ -536,7 +539,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() @@ -554,7 +557,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 }) @@ -562,7 +565,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(), @@ -574,7 +577,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(), @@ -585,7 +588,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]) From e6ec050ec3921cfcf1503d349d7596cfc320f748 Mon Sep 17 00:00:00 2001 From: Pete Christianson Date: Wed, 4 Mar 2026 00:42:08 -0800 Subject: [PATCH 2/3] Fix XSS in memory-browser-panel Replace dangerouslySetInnerHTML with React elements for inline formatting (bold/italic). New renderInlineFormatting() helper returns React nodes instead of raw HTML strings, eliminating XSS risk from user-controlled memory content. Co-Authored-By: Claude Opus 4.6 --- .../panels/memory-browser-panel.tsx | 40 +++++++++++++------ 1 file changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/panels/memory-browser-panel.tsx b/src/components/panels/memory-browser-panel.tsx index 7eeba6e..5980125 100644 --- a/src/components/panels/memory-browser-panel.tsx +++ b/src/components/panels/memory-browser-panel.tsx @@ -271,6 +271,30 @@ export function MemoryBrowserPanel() { )) } + const renderInlineFormatting = (text: string): React.ReactNode[] => { + const parts: React.ReactNode[] = [] + const regex = /(\*\*.*?\*\*|\*.*?\*)/g + let lastIndex = 0 + let match: RegExpExecArray | null + let key = 0 + while ((match = regex.exec(text)) !== null) { + if (match.index > lastIndex) { + parts.push(text.slice(lastIndex, match.index)) + } + const m = match[0] + if (m.startsWith('**') && m.endsWith('**')) { + parts.push({m.slice(2, -2)}) + } else if (m.startsWith('*') && m.endsWith('*')) { + parts.push({m.slice(1, -1)}) + } + lastIndex = regex.lastIndex + } + if (lastIndex < text.length) { + parts.push(text.slice(lastIndex)) + } + return parts + } + const renderMarkdown = (content: string) => { // Improved markdown rendering with proper line handling const lines = content.split('\n') @@ -323,20 +347,10 @@ export function MemoryBrowserPanel() { elements.push(
) } else if (trimmedLine.length > 0) { if (inList) inList = false - // Handle inline formatting — escape HTML entities first to prevent XSS - let content = trimmedLine - .replace(/&/g, '&') - .replace(//g, '>') - .replace(/"/g, '"') - .replace(/'/g, ''') - // Simple bold formatting - content = content.replace(/\*\*(.*?)\*\*/g, '$1') - // Simple italic formatting - content = content.replace(/\*(.*?)\*/g, '$1') - elements.push( -

+

+ {renderInlineFormatting(trimmedLine)} +

) } } From e8229cd290e595ad7633dc8c9e837bf5011927f9 Mon Sep 17 00:00:00 2001 From: nyk <93952610+0xNyk@users.noreply.github.com> Date: Wed, 4 Mar 2026 22:26:06 +0700 Subject: [PATCH 3/3] feat: add URL-based task deep links and agent task links --- src/components/panels/agent-detail-tabs.tsx | 6 ++- src/components/panels/task-board-panel.tsx | 60 +++++++++++++++++++-- 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/components/panels/agent-detail-tabs.tsx b/src/components/panels/agent-detail-tabs.tsx index 1714fc9..ea42600 100644 --- a/src/components/panels/agent-detail-tabs.tsx +++ b/src/components/panels/agent-detail-tabs.tsx @@ -1,6 +1,7 @@ 'use client' import { useState, useEffect } from 'react' +import Link from 'next/link' interface Agent { id: number @@ -665,7 +666,10 @@ export function TasksTab({ agent }: { agent: Agent }) {
-
{task.title}
+ + {task.title} + +
Task #{task.id}
{task.description && (

{task.description}

)} diff --git a/src/components/panels/task-board-panel.tsx b/src/components/panels/task-board-panel.tsx index 355b6ce..1dda867 100644 --- a/src/components/panels/task-board-panel.tsx +++ b/src/components/panels/task-board-panel.tsx @@ -1,6 +1,7 @@ 'use client' 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 { useFocusTrap } from '@/lib/use-focus-trap' @@ -67,6 +68,9 @@ const priorityColors: Record = { export function TaskBoardPanel() { const { tasks: storeTasks, setTasks: storeSetTasks, selectedTask, setSelectedTask } = useMissionControl() + const router = useRouter() + const pathname = usePathname() + const searchParams = useSearchParams() const [agents, setAgents] = useState([]) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) @@ -75,6 +79,23 @@ export function TaskBoardPanel() { const [showCreateModal, setShowCreateModal] = useState(false) const [editingTask, setEditingTask] = useState(null) const dragCounter = useRef(0) + const selectedTaskIdFromUrl = Number.parseInt(searchParams.get('taskId') || '', 10) + + const updateTaskUrl = useCallback((taskId: number | null, mode: 'push' | 'replace' = 'push') => { + const params = new URLSearchParams(searchParams.toString()) + if (typeof taskId === 'number' && Number.isFinite(taskId)) { + params.set('taskId', String(taskId)) + } else { + params.delete('taskId') + } + const query = params.toString() + const href = query ? `${pathname}?${query}` : pathname + if (mode === 'replace') { + router.replace(href) + return + } + router.push(href) + }, [pathname, router, searchParams]) // Augment store tasks with aegisApproved flag (computed, not stored) const tasks: Task[] = storeTasks.map(t => ({ @@ -136,6 +157,26 @@ export function TaskBoardPanel() { fetchData() }, [fetchData]) + useEffect(() => { + if (!Number.isFinite(selectedTaskIdFromUrl)) { + if (selectedTask) setSelectedTask(null) + return + } + + const match = tasks.find((task) => task.id === selectedTaskIdFromUrl) + if (match) { + if (selectedTask?.id !== match.id) { + setSelectedTask(match) + } + return + } + + if (!loading) { + setError(`Task #${selectedTaskIdFromUrl} not found in current workspace`) + setSelectedTask(null) + } + }, [loading, selectedTask, selectedTaskIdFromUrl, setSelectedTask, tasks]) + // Poll as SSE fallback — pauses when SSE is delivering events useSmartPoll(fetchData, 30000, { pauseWhenSseConnected: true }) @@ -342,8 +383,17 @@ export function TaskBoardPanel() { tabIndex={0} aria-label={`${task.title}, ${task.priority} priority, ${task.status}`} onDragStart={(e) => handleDragStart(e, task)} - onClick={() => setSelectedTask(task)} - onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); setSelectedTask(task) } }} + onClick={() => { + setSelectedTask(task) + updateTaskUrl(task.id) + }} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault() + setSelectedTask(task) + updateTaskUrl(task.id) + } + }} className={`bg-surface-1 rounded-lg p-3 cursor-pointer hover:bg-surface-2 transition-smooth border-l-4 ${priorityColors[task.priority]} ${ draggedTask?.id === task.id ? 'opacity-50' : '' }`} @@ -440,11 +490,15 @@ export function TaskBoardPanel() { setSelectedTask(null)} + onClose={() => { + setSelectedTask(null) + updateTaskUrl(null) + }} onUpdate={fetchData} onEdit={(taskToEdit) => { setEditingTask(taskToEdit) setSelectedTask(null) + updateTaskUrl(null, 'replace') }} /> )}