Merge pull request #139 from bhavikprit/feat/39-structured-logging

feat(#39): replace console.log/error/warn with structured client logger
This commit is contained in:
nyk 2026-03-04 22:49:06 +07:00 committed by GitHub
commit e3c33a6a0c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
20 changed files with 211 additions and 83 deletions

View File

@ -1,6 +1,9 @@
'use client' 'use client'
import { Component, type ErrorInfo, type ReactNode } from 'react' import { Component, type ErrorInfo, type ReactNode } from 'react'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('ErrorBoundary')
interface Props { interface Props {
children: ReactNode children: ReactNode
@ -23,7 +26,7 @@ export class ErrorBoundary extends Component<Props, State> {
} }
componentDidCatch(error: Error, errorInfo: ErrorInfo) { componentDidCatch(error: Error, errorInfo: ErrorInfo) {
console.error('Panel error:', error, errorInfo) log.error('Panel error:', error, errorInfo)
} }
render() { render() {

View File

@ -3,10 +3,13 @@
import { useEffect, useCallback, useState, useRef } from 'react' import { useEffect, useCallback, useState, useRef } from 'react'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { useSmartPoll } from '@/lib/use-smart-poll' import { useSmartPoll } from '@/lib/use-smart-poll'
import { createClientLogger } from '@/lib/client-logger'
import { ConversationList } from './conversation-list' import { ConversationList } from './conversation-list'
import { MessageList } from './message-list' import { MessageList } from './message-list'
import { ChatInput } from './chat-input' import { ChatInput } from './chat-input'
const log = createClientLogger('ChatPanel')
export function ChatPanel() { export function ChatPanel() {
const { const {
chatPanelOpen, chatPanelOpen,
@ -52,7 +55,7 @@ export function ChatPanel() {
const data = await res.json() const data = await res.json()
if (data.agents) setAgents(data.agents) if (data.agents) setAgents(data.agents)
} catch (err) { } catch (err) {
console.error('Failed to load agents:', err) log.error('Failed to load agents:', err)
} }
} }
if (chatPanelOpen) loadAgents() if (chatPanelOpen) loadAgents()
@ -67,7 +70,7 @@ export function ChatPanel() {
const data = await res.json() const data = await res.json()
if (data.messages) setChatMessages(data.messages) if (data.messages) setChatMessages(data.messages)
} catch (err) { } catch (err) {
console.error('Failed to load messages:', err) log.error('Failed to load messages:', err)
} }
}, [activeConversation, setChatMessages]) }, [activeConversation, setChatMessages])
@ -145,7 +148,7 @@ export function ChatPanel() {
updatePendingMessage(tempId, { pendingStatus: 'failed' }) updatePendingMessage(tempId, { pendingStatus: 'failed' })
} }
} catch (err) { } catch (err) {
console.error('Failed to send message:', err) log.error('Failed to send message:', err)
updatePendingMessage(tempId, { pendingStatus: 'failed' }) updatePendingMessage(tempId, { pendingStatus: 'failed' })
} }
} }

View File

@ -3,6 +3,9 @@
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { useMissionControl, Conversation, Agent } from '@/store' import { useMissionControl, Conversation, Agent } from '@/store'
import { useSmartPoll } from '@/lib/use-smart-poll' import { useSmartPoll } from '@/lib/use-smart-poll'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('ConversationList')
function timeAgo(timestamp: number): string { function timeAgo(timestamp: number): string {
const diff = Math.floor(Date.now() / 1000) - timestamp const diff = Math.floor(Date.now() / 1000) - timestamp
@ -64,7 +67,7 @@ export function ConversationList({ onNewConversation }: ConversationListProps) {
) )
} }
} catch (err) { } catch (err) {
console.error('Failed to load conversations:', err) log.error('Failed to load conversations:', err)
} }
}, [setConversations]) }, [setConversations])

View File

@ -3,6 +3,9 @@
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { useNavigateToPanel } from '@/lib/navigation' import { useNavigateToPanel } from '@/lib/navigation'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('Sidebar')
interface MenuItem { interface MenuItem {
id: string id: string
@ -36,7 +39,7 @@ export function Sidebar() {
fetch('/api/status?action=overview') fetch('/api/status?action=overview')
.then(res => res.json()) .then(res => res.json())
.then(data => setSystemStats(data)) .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 const activeSessions = sessions.filter(s => s.active).length

View File

@ -1,11 +1,14 @@
'use client' 'use client'
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { createClientLogger } from '@/lib/client-logger'
import { import {
PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, CartesianGrid, PieChart, Pie, Cell, LineChart, Line, XAxis, YAxis, CartesianGrid,
Tooltip, Legend, ResponsiveContainer, Tooltip, Legend, ResponsiveContainer,
} from 'recharts' } from 'recharts'
const log = createClientLogger('AgentCostPanel')
interface AgentCostData { interface AgentCostData {
stats: { totalTokens: number; totalCost: number; requestCount: number; avgTokensPerRequest: number; avgCostPerRequest: number } stats: { totalTokens: number; totalCost: number; requestCount: number; avgTokensPerRequest: number; avgCostPerRequest: number }
models: Record<string, { totalTokens: number; totalCost: number; requestCount: number }> models: Record<string, { totalTokens: number; totalCost: number; requestCount: number }>
@ -34,7 +37,7 @@ export function AgentCostPanel() {
const json = await res.json() const json = await res.json()
setData(json) setData(json)
} catch (err) { } catch (err) {
console.error('Failed to load agent costs:', err) log.error('Failed to load agent costs:', err)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }

View File

@ -1,8 +1,11 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('AgentDetailTabs')
import Link from 'next/link' import Link from 'next/link'
interface Agent { interface Agent {
id: number id: number
name: string name: string
@ -374,7 +377,7 @@ export function SoulTab({
setSelectedTemplate(templateName) setSelectedTemplate(templateName)
} }
} catch (error) { } 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 || []) setTasks(data.tasks || [])
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch tasks:', error) log.error('Failed to fetch tasks:', error)
} finally { } finally {
setLoading(false) setLoading(false)
} }
@ -722,7 +725,7 @@ export function ActivityTab({ agent }: { agent: Agent }) {
setActivities(data.activities || []) setActivities(data.activities || [])
} }
} catch (error) { } catch (error) {
console.error('Failed to fetch activities:', error) log.error('Failed to fetch activities:', error)
} finally { } finally {
setLoading(false) setLoading(false)
} }

View File

@ -2,6 +2,9 @@
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('AgentSpawnPanel')
interface SpawnFormData { interface SpawnFormData {
task: string task: string
@ -33,7 +36,7 @@ export function AgentSpawnPanel() {
fetch('/api/spawn') fetch('/api/spawn')
.then(res => res.json()) .then(res => res.json())
.then(data => setSpawnHistory(data.history || [])) .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 () => { const handleSpawn = async () => {
@ -95,7 +98,7 @@ export function AgentSpawnPanel() {
}) })
} }
} catch (error) { } catch (error) {
console.error('Spawn error:', error) log.error('Spawn error:', error)
updateSpawnRequest(spawnId, { updateSpawnRequest(spawnId, {
status: 'failed', status: 'failed',
error: error instanceof Error ? error.message : 'Network error' error: error instanceof Error ? error.message : 'Network error'

View File

@ -2,6 +2,7 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useSmartPoll } from '@/lib/use-smart-poll' import { useSmartPoll } from '@/lib/use-smart-poll'
import { createClientLogger } from '@/lib/client-logger'
import { AgentAvatar } from '@/components/ui/agent-avatar' import { AgentAvatar } from '@/components/ui/agent-avatar'
import { import {
OverviewTab, OverviewTab,
@ -13,6 +14,8 @@ import {
CreateAgentModal CreateAgentModal
} from './agent-detail-tabs' } from './agent-detail-tabs'
const log = createClientLogger('AgentSquadPhase3')
interface Agent { interface Agent {
id: number id: number
name: string name: string
@ -148,7 +151,7 @@ export function AgentSquadPanelPhase3() {
: agent : agent
)) ))
} catch (error) { } catch (error) {
console.error('Failed to update agent status:', error) log.error('Failed to update agent status:', error)
setError('Failed to update agent status') setError('Failed to update agent status')
} }
} }
@ -171,7 +174,7 @@ export function AgentSquadPanelPhase3() {
await updateAgentStatus(agentName, 'idle', 'Manually woken via session') await updateAgentStatus(agentName, 'idle', 'Manually woken via session')
} catch (error) { } catch (error) {
console.error('Failed to wake agent:', error) log.error('Failed to wake agent:', error)
setError('Failed to wake agent') setError('Failed to wake agent')
} }
} }
@ -503,7 +506,7 @@ function AgentDetailModalPhase3({
setSoulTemplates(data.templates || []) setSoulTemplates(data.templates || [])
} }
} catch (error) { } 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) setHeartbeatData(data)
} }
} catch (error) { } catch (error) {
console.error('Failed to perform heartbeat:', error) log.error('Failed to perform heartbeat:', error)
} finally { } finally {
setLoadingHeartbeat(false) setLoadingHeartbeat(false)
} }
@ -544,7 +547,7 @@ function AgentDetailModalPhase3({
setEditing(false) setEditing(false)
onUpdate() onUpdate()
} catch (error) { } 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 })) setFormData(prev => ({ ...prev, soul_content: content }))
onUpdate() onUpdate()
} catch (error) { } 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 })) setFormData(prev => ({ ...prev, working_memory: data.working_memory }))
onUpdate() onUpdate()
} catch (error) { } 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') alert(result.error || 'Failed to spawn agent')
} }
} catch (error) { } catch (error) {
console.error('Spawn failed:', error) log.error('Spawn failed:', error)
alert('Network error occurred') alert('Network error occurred')
} finally { } finally {
setIsSpawning(false) setIsSpawning(false)

View File

@ -1,6 +1,9 @@
'use client' 'use client'
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('AgentSquadPanel')
interface Agent { interface Agent {
id: number id: number
@ -103,7 +106,7 @@ export function AgentSquadPanel() {
: agent : agent
)) ))
} catch (error) { } catch (error) {
console.error('Failed to update agent status:', error) log.error('Failed to update agent status:', error)
setError('Failed to update agent status') setError('Failed to update agent status')
} }
} }
@ -355,7 +358,7 @@ function AgentDetailModal({
setEditing(false) setEditing(false)
onUpdate() onUpdate()
} catch (error) { } catch (error) {
console.error('Failed to update agent:', error) log.error('Failed to update agent:', error)
} }
} }
@ -540,7 +543,7 @@ function CreateAgentModal({
onCreated() onCreated()
onClose() onClose()
} catch (error) { } catch (error) {
console.error('Error creating agent:', error) log.error('Error creating agent:', error)
} }
} }

View File

@ -2,6 +2,8 @@
import { useState, useEffect, useCallback, useMemo } from 'react' import { useState, useEffect, useCallback, useMemo } from 'react'
import { useMissionControl, CronJob } from '@/store' import { useMissionControl, CronJob } from '@/store'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('CronManagement')
import { buildDayKey, getCronOccurrences } from '@/lib/cron-occurrences' import { buildDayKey, getCronOccurrences } from '@/lib/cron-occurrences'
interface NewJobForm { interface NewJobForm {
@ -92,7 +94,7 @@ export function CronManagementPanel() {
const data = await response.json() const data = await response.json()
setCronJobs(data.jobs || []) setCronJobs(data.jobs || [])
} catch (error) { } catch (error) {
console.error('Failed to load cron jobs:', error) log.error('Failed to load cron jobs:', error)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -126,7 +128,7 @@ export function CronManagementPanel() {
const data = await response.json() const data = await response.json()
setJobLogs(data.logs || []) setJobLogs(data.logs || [])
} catch (error) { } catch (error) {
console.error('Failed to load job logs:', error) log.error('Failed to load job logs:', error)
setJobLogs([]) setJobLogs([])
} }
} }
@ -150,7 +152,7 @@ export function CronManagementPanel() {
alert(`Failed to toggle job: ${error.error}`) alert(`Failed to toggle job: ${error.error}`)
} }
} catch (error) { } catch (error) {
console.error('Failed to toggle job:', error) log.error('Failed to toggle job:', error)
alert('Network error occurred') alert('Network error occurred')
} }
} }
@ -175,7 +177,7 @@ export function CronManagementPanel() {
alert(`Job failed:\n${result.error}\n${result.stderr}`) alert(`Job failed:\n${result.error}\n${result.stderr}`)
} }
} catch (error) { } catch (error) {
console.error('Failed to trigger job:', error) log.error('Failed to trigger job:', error)
alert('Network error occurred') alert('Network error occurred')
} }
} }
@ -214,7 +216,7 @@ export function CronManagementPanel() {
alert(`Failed to add job: ${error.error}`) alert(`Failed to add job: ${error.error}`)
} }
} catch (error) { } catch (error) {
console.error('Failed to add job:', error) log.error('Failed to add job:', error)
alert('Network error occurred') alert('Network error occurred')
} }
} }
@ -244,7 +246,7 @@ export function CronManagementPanel() {
alert(`Failed to remove job: ${error.error}`) alert(`Failed to remove job: ${error.error}`)
} }
} catch (error) { } catch (error) {
console.error('Failed to remove job:', error) log.error('Failed to remove job:', error)
alert('Network error occurred') alert('Network error occurred')
} }
} }

View File

@ -3,6 +3,9 @@
import { useState, useEffect, useRef, useCallback } from 'react' import { useState, useEffect, useRef, useCallback } from 'react'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { useSmartPoll } from '@/lib/use-smart-poll' import { useSmartPoll } from '@/lib/use-smart-poll'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('LogViewer')
interface LogFilters { interface LogFilters {
level?: string level?: string
@ -36,7 +39,7 @@ export function LogViewerPanel() {
}, [logFilters]) }, [logFilters])
const loadLogs = useCallback(async (tail = false) => { 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 setIsLoading(!tail) // Only show loading for initial load, not for tailing
try { try {
@ -53,37 +56,37 @@ export function LogViewerPanel() {
...(tail && currentLogs.length > 0 && { since: currentLogs[0]?.timestamp.toString() }) ...(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 response = await fetch(`/api/logs?${params}`)
const data = await response.json() 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 (data.logs && data.logs.length > 0) {
if (tail) { if (tail) {
// Add new logs for tail mode - prepend to existing logs // Add new logs for tail mode - prepend to existing logs
let newLogsAdded = 0 let newLogsAdded = 0
const existingIds = new Set((currentLogs || []).map((l: any) => l?.id).filter(Boolean)) const existingIds = new Set((currentLogs || []).map((l: any) => l?.id).filter(Boolean))
data.logs.reverse().forEach((log: any) => { data.logs.reverse().forEach((entry: any) => {
if (existingIds.has(log?.id)) return if (existingIds.has(entry?.id)) return
addLog(log) addLog(entry)
newLogsAdded++ newLogsAdded++
}) })
console.log(`LogViewer: Added ${newLogsAdded} new logs (tail mode)`) log.debug(`Added ${newLogsAdded} new logs (tail mode)`)
} else { } else {
// Replace logs for initial load or refresh // 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 clearLogs() // Clear existing logs
data.logs.reverse().forEach((log: any) => { data.logs.reverse().forEach((entry: any) => {
addLog(log) addLog(entry)
}) })
console.log(`LogViewer: Successfully added ${data.logs.length} logs to store`) log.debug(`Successfully added ${data.logs.length} logs to store`)
} }
} else { } else {
console.log('LogViewer: No logs received from API') log.debug('No logs received from API')
} }
} catch (error) { } catch (error) {
console.error('LogViewer: Failed to load logs:', error) log.error('Failed to load logs:', error)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -95,13 +98,13 @@ export function LogViewerPanel() {
const data = await response.json() const data = await response.json()
setAvailableSources(data.sources || []) setAvailableSources(data.sources || [])
} catch (error) { } catch (error) {
console.error('Failed to load log sources:', error) log.error('Failed to load log sources:', error)
} }
}, []) }, [])
// Load initial logs and sources // Load initial logs and sources
useEffect(() => { useEffect(() => {
console.log('LogViewer: Initial load started') log.debug('Initial load started')
loadLogs() loadLogs()
loadSources() loadSources()
}, [loadLogs, loadSources]) }, [loadLogs, loadSources])
@ -154,16 +157,16 @@ export function LogViewerPanel() {
} }
} }
const filteredLogs = logs.filter(log => { const filteredLogs = logs.filter(entry => {
if (logFilters.level && log.level !== logFilters.level) return false if (logFilters.level && entry.level !== logFilters.level) return false
if (logFilters.source && log.source !== logFilters.source) return false if (logFilters.source && entry.source !== logFilters.source) return false
if (logFilters.search && !log.message.toLowerCase().includes(logFilters.search.toLowerCase())) return false if (logFilters.search && !entry.message.toLowerCase().includes(logFilters.search.toLowerCase())) return false
if (logFilters.session && (!log.session || !log.session.includes(logFilters.session))) return false if (logFilters.session && (!entry.session || !entry.session.includes(logFilters.session))) return false
return true return true
}) })
// Debug logging // 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 ( return (
<div className="flex flex-col h-full p-6 space-y-4"> <div className="flex flex-col h-full p-6 space-y-4">

View File

@ -2,6 +2,9 @@
import React, { useState, useEffect, useCallback } from 'react' import React, { useState, useEffect, useCallback } from 'react'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('MemoryBrowser')
interface MemoryFile { interface MemoryFile {
path: string path: string
@ -46,7 +49,7 @@ export function MemoryBrowserPanel() {
// Auto-expand some common directories // Auto-expand some common directories
setExpandedFolders(new Set(['daily', 'knowledge'])) setExpandedFolders(new Set(['daily', 'knowledge']))
} catch (error) { } catch (error) {
console.error('Failed to load file tree:', error) log.error('Failed to load file tree:', error)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -83,7 +86,7 @@ export function MemoryBrowserPanel() {
alert(data.error || 'Failed to load file content') alert(data.error || 'Failed to load file content')
} }
} catch (error) { } catch (error) {
console.error('Failed to load file content:', error) log.error('Failed to load file content:', error)
alert('Network error occurred') alert('Network error occurred')
} finally { } finally {
setIsLoading(false) setIsLoading(false)
@ -99,7 +102,7 @@ export function MemoryBrowserPanel() {
const data = await response.json() const data = await response.json()
setSearchResults(data.results || []) setSearchResults(data.results || [])
} catch (error) { } catch (error) {
console.error('Search failed:', error) log.error('Search failed:', error)
setSearchResults([]) setSearchResults([])
} finally { } finally {
setIsSearching(false) setIsSearching(false)
@ -165,7 +168,7 @@ export function MemoryBrowserPanel() {
alert(data.error || 'Failed to save file') alert(data.error || 'Failed to save file')
} }
} catch (error) { } catch (error) {
console.error('Failed to save file:', error) log.error('Failed to save file:', error)
alert('Network error occurred') alert('Network error occurred')
} finally { } finally {
setIsSaving(false) setIsSaving(false)
@ -192,7 +195,7 @@ export function MemoryBrowserPanel() {
alert(data.error || 'Failed to create file') alert(data.error || 'Failed to create file')
} }
} catch (error) { } catch (error) {
console.error('Failed to create file:', error) log.error('Failed to create file:', error)
alert('Network error occurred') alert('Network error occurred')
} }
} }
@ -220,7 +223,7 @@ export function MemoryBrowserPanel() {
alert(data.error || 'Failed to delete file') alert(data.error || 'Failed to delete file')
} }
} catch (error) { } catch (error) {
console.error('Failed to delete file:', error) log.error('Failed to delete file:', error)
alert('Network error occurred') alert('Network error occurred')
} }
} }

View File

@ -3,6 +3,9 @@
import { useState, useCallback } from 'react' import { useState, useCallback } from 'react'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { useSmartPoll } from '@/lib/use-smart-poll' import { useSmartPoll } from '@/lib/use-smart-poll'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('SessionDetails')
export function SessionDetailsPanel() { export function SessionDetailsPanel() {
const { const {
@ -20,7 +23,7 @@ export function SessionDetailsPanel() {
const data = await response.json() const data = await response.json()
setSessions(data.sessions || data) setSessions(data.sessions || data)
} catch (error) { } catch (error) {
console.error('Failed to load sessions:', error) log.error('Failed to load sessions:', error)
} }
}, [setSessions]) }, [setSessions])

View File

@ -1,6 +1,9 @@
'use client' 'use client'
import { useState, useEffect } from 'react' import { useState, useEffect } from 'react'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('StandupPanel')
interface StandupReport { interface StandupReport {
date: string date: string
@ -133,7 +136,7 @@ export function StandupPanel() {
const data = await response.json() const data = await response.json()
setStandupHistory(data.history || []) setStandupHistory(data.history || [])
} catch (err) { } catch (err) {
console.error('Failed to fetch standup history:', err) log.error('Failed to fetch standup history:', err)
} }
} }

View File

@ -4,10 +4,16 @@ import { useState, useEffect, useCallback, useRef } from 'react'
import { usePathname, useRouter, useSearchParams } from 'next/navigation' import { usePathname, useRouter, useSearchParams } from 'next/navigation'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { useSmartPoll } from '@/lib/use-smart-poll' import { useSmartPoll } from '@/lib/use-smart-poll'
import { createClientLogger } from '@/lib/client-logger'
import { useFocusTrap } from '@/lib/use-focus-trap' import { useFocusTrap } from '@/lib/use-focus-trap'
import { AgentAvatar } from '@/components/ui/agent-avatar' import { AgentAvatar } from '@/components/ui/agent-avatar'
import { MarkdownRenderer } from '@/components/markdown-renderer' import { MarkdownRenderer } from '@/components/markdown-renderer'
const log = createClientLogger('TaskBoard')
interface Task { interface Task {
id: number id: number
title: string title: string
@ -937,7 +943,7 @@ function CreateTaskModal({
onCreated() onCreated()
onClose() onClose()
} catch (error) { } catch (error) {
console.error('Error creating task:', error) log.error('Error creating task:', error)
} }
} }
@ -1086,7 +1092,7 @@ function EditTaskModal({
onUpdated() onUpdated()
} catch (error) { } catch (error) {
console.error('Error updating task:', error) log.error('Error updating task:', error)
} }
} }

View File

@ -2,8 +2,11 @@
import { useState, useEffect, useCallback } from 'react' import { useState, useEffect, useCallback } from 'react'
import { useMissionControl } from '@/store' 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' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend, ResponsiveContainer, BarChart, Bar, PieChart, Pie, Cell } from 'recharts'
const log = createClientLogger('TokenDashboard')
interface UsageStats { interface UsageStats {
summary: { summary: {
totalTokens: number totalTokens: number
@ -39,7 +42,7 @@ export function TokenDashboardPanel() {
const data = await response.json() const data = await response.json()
setUsageStats(data) setUsageStats(data)
} catch (error) { } catch (error) {
console.error('Failed to load usage stats:', error) log.error('Failed to load usage stats:', error)
} finally { } finally {
setIsLoading(false) setIsLoading(false)
} }
@ -51,7 +54,7 @@ export function TokenDashboardPanel() {
const data = await response.json() const data = await response.json()
setTrendData(data) setTrendData(data)
} catch (error) { } catch (error) {
console.error('Failed to load trend data:', error) log.error('Failed to load trend data:', error)
} }
}, [selectedTimeframe]) }, [selectedTimeframe])
@ -80,7 +83,7 @@ export function TokenDashboardPanel() {
window.URL.revokeObjectURL(url) window.URL.revokeObjectURL(url)
document.body.removeChild(a) document.body.removeChild(a)
} catch (error) { } catch (error) {
console.error('Export failed:', error) log.error('Export failed:', error)
alert('Export failed: ' + error) alert('Export failed: ' + error)
} finally { } finally {
setIsExporting(false) setIsExporting(false)

68
src/lib/client-logger.ts Normal file
View File

@ -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<LogLevel, number> = {
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<string, unknown>, msg?: string): void
info(msg: string, ...args: unknown[]): void
info(obj: Record<string, unknown>, msg?: string): void
warn(msg: string, ...args: unknown[]): void
warn(obj: Record<string, unknown>, msg?: string): void
error(msg: string, ...args: unknown[]): void
error(obj: Record<string, unknown>, 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))
},
}
}

View File

@ -1,5 +1,9 @@
'use client' 'use client'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('DeviceIdentity')
/** /**
* Ed25519 device identity for OpenClaw gateway protocol v3 challenge-response. * Ed25519 device identity for OpenClaw gateway protocol v3 challenge-response.
* *
@ -101,7 +105,7 @@ export async function getOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
} }
} catch { } catch {
// Stored key corrupted — regenerate // Stored key corrupted — regenerate
console.warn('Device identity keys corrupted, regenerating...') log.warn('Device identity keys corrupted, regenerating...')
} }
} }

View File

@ -2,6 +2,9 @@
import { useEffect, useRef } from 'react' import { useEffect, useRef } from 'react'
import { useMissionControl } from '@/store' import { useMissionControl } from '@/store'
import { createClientLogger } from '@/lib/client-logger'
const log = createClientLogger('SSE')
interface ServerEvent { interface ServerEvent {
type: string type: string
@ -73,7 +76,7 @@ export function useServerEvents() {
const attempts = sseReconnectAttemptsRef.current const attempts = sseReconnectAttemptsRef.current
if (attempts >= SSE_MAX_RECONNECT_ATTEMPTS) { 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 return
} }
@ -82,7 +85,7 @@ export function useServerEvents() {
const delay = Math.round(base + Math.random() * base * 0.5) const delay = Math.round(base + Math.random() * base * 0.5)
sseReconnectAttemptsRef.current = attempts + 1 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(() => { reconnectTimeoutRef.current = setTimeout(() => {
if (mounted) connect() if (mounted) connect()
}, delay) }, delay)

View File

@ -10,6 +10,9 @@ import {
cacheDeviceToken, cacheDeviceToken,
} from '@/lib/device-identity' } from '@/lib/device-identity'
import { APP_VERSION } from '@/lib/version' 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) // Gateway protocol version (v3 required by OpenClaw 2026.x)
const PROTOCOL_VERSION = 3 const PROTOCOL_VERSION = 3
@ -116,7 +119,7 @@ export function useWebSocket() {
// Check missed pongs // Check missed pongs
if (missedPongsRef.current >= MAX_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({ addLog({
id: `heartbeat-${Date.now()}`, id: `heartbeat-${Date.now()}`,
timestamp: Date.now(), timestamp: Date.now(),
@ -213,7 +216,7 @@ export function useWebSocket() {
nonce, nonce,
} }
} catch (err) { } 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, deviceToken: cachedToken || undefined,
} }
} }
console.log('Sending connect handshake:', connectRequest) log.info('Sending connect handshake')
ws.send(JSON.stringify(connectRequest)) ws.send(JSON.stringify(connectRequest))
}, []) }, [])
@ -249,7 +252,7 @@ export function useWebSocket() {
// Debug logging for development // Debug logging for development
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
console.log('WebSocket message received:', message.type, message) log.debug(`Message received: ${message.type}`)
} }
switch (message.type) { switch (message.type) {
@ -319,24 +322,24 @@ export function useWebSocket() {
break break
default: default:
console.log('Unknown gateway message type:', message.type) log.warn(`Unknown gateway message type: ${message.type}`)
} }
}, [setLastMessage, setSessions, addLog, updateSpawnRequest, setCronJobs, addTokenUsage]) }, [setLastMessage, setSessions, addLog, updateSpawnRequest, setCronJobs, addTokenUsage])
// Handle gateway protocol frames // Handle gateway protocol frames
const handleGatewayFrame = useCallback((frame: GatewayFrame, ws: WebSocket) => { const handleGatewayFrame = useCallback((frame: GatewayFrame, ws: WebSocket) => {
console.log('Gateway frame:', frame) log.debug(`Gateway frame: ${frame.type}`)
// Handle connect challenge // Handle connect challenge
if (frame.type === 'event' && frame.event === '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) sendConnectHandshake(ws, frame.payload?.nonce)
return return
} }
// Handle connect response (handshake success) // Handle connect response (handshake success)
if (frame.type === 'res' && frame.ok && !handshakeCompleteRef.current) { if (frame.type === 'res' && frame.ok && !handshakeCompleteRef.current) {
console.log('Handshake complete!') log.info('Handshake complete')
handshakeCompleteRef.current = true handshakeCompleteRef.current = true
reconnectAttemptsRef.current = 0 reconnectAttemptsRef.current = 0
// Cache device token if returned by gateway // Cache device token if returned by gateway
@ -361,7 +364,7 @@ export function useWebSocket() {
// Handle connect error // Handle connect error
if (frame.type === 'res' && !frame.ok) { 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 rawMessage = frame.error?.message || JSON.stringify(frame.error)
const help = getGatewayErrorHelp(rawMessage) const help = getGatewayErrorHelp(rawMessage)
const nonRetryable = isNonRetryableGatewayError(rawMessage) const nonRetryable = isNonRetryableGatewayError(rawMessage)
@ -510,14 +513,14 @@ export function useWebSocket() {
wsRef.current = ws wsRef.current = ws
ws.onopen = () => { 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 // Don't set isConnected yet - wait for handshake
setConnection({ setConnection({
url: url.split('?')[0], url: url.split('?')[0],
reconnectAttempts: 0 reconnectAttempts: 0
}) })
// Wait for connect.challenge from server // Wait for connect.challenge from server
console.log('Waiting for connect challenge...') log.debug('Waiting for connect challenge')
} }
ws.onmessage = (event) => { ws.onmessage = (event) => {
@ -525,7 +528,7 @@ export function useWebSocket() {
const frame = JSON.parse(event.data) as GatewayFrame const frame = JSON.parse(event.data) as GatewayFrame
handleGatewayFrame(frame, ws) handleGatewayFrame(frame, ws)
} catch (error) { } catch (error) {
console.error('Failed to parse WebSocket message:', error) log.error('Failed to parse WebSocket message:', error)
addLog({ addLog({
id: `raw-${Date.now()}`, id: `raw-${Date.now()}`,
timestamp: Date.now(), timestamp: Date.now(),
@ -537,7 +540,7 @@ export function useWebSocket() {
} }
ws.onclose = (event) => { ws.onclose = (event) => {
console.log('Disconnected from Gateway:', event.code, event.reason) log.info(`Disconnected from Gateway: ${event.code} ${event.reason}`)
setConnection({ isConnected: false }) setConnection({ isConnected: false })
handshakeCompleteRef.current = false handshakeCompleteRef.current = false
stopHeartbeat() stopHeartbeat()
@ -555,7 +558,7 @@ export function useWebSocket() {
if (attempts < maxReconnectAttempts) { if (attempts < maxReconnectAttempts) {
const base = Math.min(Math.pow(2, attempts) * 1000, 30000) const base = Math.min(Math.pow(2, attempts) * 1000, 30000)
const timeout = Math.round(base + Math.random() * base * 0.5) 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 reconnectAttemptsRef.current = attempts + 1
setConnection({ reconnectAttempts: attempts + 1 }) setConnection({ reconnectAttempts: attempts + 1 })
@ -563,7 +566,7 @@ export function useWebSocket() {
connectRef.current(reconnectUrl.current, authTokenRef.current) connectRef.current(reconnectUrl.current, authTokenRef.current)
}, timeout) }, timeout)
} else { } else {
console.error('Max reconnection attempts reached.') log.error('Max reconnection attempts reached')
addLog({ addLog({
id: `error-${Date.now()}`, id: `error-${Date.now()}`,
timestamp: Date.now(), timestamp: Date.now(),
@ -575,7 +578,7 @@ export function useWebSocket() {
} }
ws.onerror = (error) => { ws.onerror = (error) => {
console.error('WebSocket error:', error) log.error('WebSocket error:', error)
addLog({ addLog({
id: `error-${Date.now()}`, id: `error-${Date.now()}`,
timestamp: Date.now(), timestamp: Date.now(),
@ -586,7 +589,7 @@ export function useWebSocket() {
} }
} catch (error) { } catch (error) {
console.error('Failed to connect to WebSocket:', error) log.error('Failed to connect to WebSocket:', error)
setConnection({ isConnected: false }) setConnection({ isConnected: false })
} }
}, [setConnection, handleGatewayFrame, addLog, stopHeartbeat]) }, [setConnection, handleGatewayFrame, addLog, stopHeartbeat])