From e673b04a74054e497b36b041efaaedf09c90ff71 Mon Sep 17 00:00:00 2001 From: 1batu <32592602+1batu@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:01:58 +0300 Subject: [PATCH 1/6] Add type checks for agent message handling Added explicit type validation for agent_thinking and agent_decision message data in ReasoningFeed to prevent invalid data from being processed. This improves robustness against malformed WebSocket messages. --- frontend/components/ReasoningFeed.tsx | 61 +++++++++++++++++---------- 1 file changed, 38 insertions(+), 23 deletions(-) diff --git a/frontend/components/ReasoningFeed.tsx b/frontend/components/ReasoningFeed.tsx index 87c537a..1109034 100644 --- a/frontend/components/ReasoningFeed.tsx +++ b/frontend/components/ReasoningFeed.tsx @@ -35,32 +35,47 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM if (lastMessage.type === 'agent_thinking') { const agentId = lastMessage.data.agent_id; - setThinkingAgents(prev => new Set(prev).add(agentId)); + if (typeof agentId === 'string') { + setThinkingAgents(prev => new Set(prev).add(agentId)); + } } if (lastMessage.type === 'agent_decision') { - const decision: Decision = { - agent_id: lastMessage.data.agent_id, - agent_name: lastMessage.data.agent_name, - decision_id: lastMessage.data.decision_id, - action: lastMessage.data.action, - stock_symbol: lastMessage.data.stock_symbol, - quantity: lastMessage.data.quantity, - reasoning_summary: lastMessage.data.reasoning_summary, - confidence: lastMessage.data.confidence, - risk_level: lastMessage.data.risk_level, - thinking_steps: lastMessage.data.thinking_steps || [], - timestamp: lastMessage.data.timestamp || lastMessage.timestamp, - }; - - setDecisions(prev => [decision, ...prev].slice(0, 10)); - - // Remove from thinking agents - setThinkingAgents(prev => { - const next = new Set(prev); - next.delete(decision.agent_id); - return next; - }); + const data = lastMessage.data; + if ( + typeof data.agent_id === 'string' && + typeof data.agent_name === 'string' && + typeof data.decision_id === 'string' && + typeof data.action === 'string' && + typeof data.stock_symbol === 'string' && + typeof data.quantity === 'number' && + typeof data.reasoning_summary === 'string' && + typeof data.confidence === 'number' && + typeof data.risk_level === 'string' + ) { + const decision: Decision = { + agent_id: data.agent_id, + agent_name: data.agent_name, + decision_id: data.decision_id, + action: data.action as 'BUY' | 'SELL' | 'HOLD', + stock_symbol: data.stock_symbol, + quantity: data.quantity, + reasoning_summary: data.reasoning_summary, + confidence: data.confidence, + risk_level: data.risk_level as 'low' | 'medium' | 'high', + thinking_steps: Array.isArray(data.thinking_steps) ? data.thinking_steps as Array<{ step: string; observation: string }> : [], + timestamp: typeof data.timestamp === 'number' ? data.timestamp : lastMessage.timestamp, + }; + + setDecisions(prev => [decision, ...prev].slice(0, 10)); + + // Remove from thinking agents + setThinkingAgents(prev => { + const next = new Set(prev); + next.delete(decision.agent_id); + return next; + }); + } } }, [lastMessage]); From cd078430f940265d33e1bfd7f202340b7ed258cd Mon Sep 17 00:00:00 2001 From: 1batu <32592602+1batu@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:06:12 +0300 Subject: [PATCH 2/6] Improve agent_thinking message validation Added stricter checks for the presence and type of agent_id in agent_thinking WebSocket messages to prevent potential errors from malformed data. --- frontend/components/ReasoningFeed.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/frontend/components/ReasoningFeed.tsx b/frontend/components/ReasoningFeed.tsx index 1109034..d0bbb37 100644 --- a/frontend/components/ReasoningFeed.tsx +++ b/frontend/components/ReasoningFeed.tsx @@ -34,9 +34,12 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM if (!lastMessage) return; if (lastMessage.type === 'agent_thinking') { - const agentId = lastMessage.data.agent_id; - if (typeof agentId === 'string') { - setThinkingAgents(prev => new Set(prev).add(agentId)); + const data = lastMessage.data; + if (data && typeof data === 'object' && 'agent_id' in data) { + const agentId = data.agent_id; + if (typeof agentId === 'string' && agentId) { + setThinkingAgents(prev => new Set(prev).add(agentId)); + } } } From 2500b547cfeae67add2f55b7b87773309efe58bd Mon Sep 17 00:00:00 2001 From: 1batu <32592602+1batu@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:09:11 +0300 Subject: [PATCH 3/6] Update ReasoningFeed.tsx --- frontend/components/ReasoningFeed.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/components/ReasoningFeed.tsx b/frontend/components/ReasoningFeed.tsx index d0bbb37..3924b4e 100644 --- a/frontend/components/ReasoningFeed.tsx +++ b/frontend/components/ReasoningFeed.tsx @@ -36,7 +36,7 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM if (lastMessage.type === 'agent_thinking') { const data = lastMessage.data; if (data && typeof data === 'object' && 'agent_id' in data) { - const agentId = data.agent_id; + const agentId = (data as Record).agent_id; if (typeof agentId === 'string' && agentId) { setThinkingAgents(prev => new Set(prev).add(agentId)); } From 2fd0c6faaa5dd31f674a40f38f9e072f51fc3da7 Mon Sep 17 00:00:00 2001 From: 1batu <32592602+1batu@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:15:08 +0300 Subject: [PATCH 4/6] Add type guards for WebSocket message data Introduced stricter type checking and filtering for WebSocket message data in LatestNews, Leaderboard, and ReasoningFeed components. This improves robustness by ensuring only valid and expected data shapes are processed, reducing the risk of runtime errors from malformed messages. Also updated WebSocketMessage interface to use 'unknown' for the data property. --- frontend/components/LatestNews.tsx | 22 +++++++++++++-------- frontend/components/Leaderboard.tsx | 28 ++++++++++++++++++++++----- frontend/components/ReasoningFeed.tsx | 15 ++++++++------ frontend/lib/websocket.ts | 2 +- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/frontend/components/LatestNews.tsx b/frontend/components/LatestNews.tsx index d5db356..59982ff 100644 --- a/frontend/components/LatestNews.tsx +++ b/frontend/components/LatestNews.tsx @@ -1,5 +1,6 @@ 'use client'; +import { type WebSocketMessage } from '@/lib/websocket'; import { formatDistanceToNow } from 'date-fns'; import { tr } from 'date-fns/locale'; import { AlertCircle, ExternalLink, Newspaper, TrendingUp } from 'lucide-react'; @@ -16,12 +17,6 @@ interface NewsArticle { impact_level?: string; } -interface WebSocketMessage { - type: string; - data: NewsArticle[] | Record; - timestamp: number; -} - export default function LatestNews({ lastMessage }: { lastMessage: WebSocketMessage | null }) { const [news, setNews] = useState([]); @@ -29,8 +24,19 @@ export default function LatestNews({ lastMessage }: { lastMessage: WebSocketMess useEffect(() => { if (!lastMessage || lastMessage.type !== 'news_update') return; - const articles = (lastMessage.data as NewsArticle[]) || []; - setNews(articles); + const data = lastMessage.data; + if (Array.isArray(data)) { + const articles = data.filter((article): article is NewsArticle => + article && + typeof article === 'object' && + typeof article.id === 'string' && + typeof article.title === 'string' && + typeof article.source === 'string' && + typeof article.url === 'string' && + typeof article.published_at === 'string' + ); + setNews(articles); + } }, [lastMessage]); const getImpactColor = (level?: string) => { diff --git a/frontend/components/Leaderboard.tsx b/frontend/components/Leaderboard.tsx index fa6a5ef..5f7607a 100644 --- a/frontend/components/Leaderboard.tsx +++ b/frontend/components/Leaderboard.tsx @@ -47,11 +47,29 @@ export default function Leaderboard() { useWebSocket('ws://localhost:8080/ws', { onMessage: (msg) => { if (msg.type === 'leaderboard_updated') { - setEntries(msg.data); - // After a leaderboard update, refresh ROI history if stale (>30s) - const now = Date.now(); - if (now - lastHistoryFetch > 30000) { - fetchROIHistory(); + const data = msg.data; + if (Array.isArray(data)) { + const entries = data.filter((entry): entry is LeaderboardEntry => + entry && + typeof entry === 'object' && + typeof entry.rank === 'number' && + typeof entry.agent_id === 'string' && + typeof entry.agent_name === 'string' && + typeof entry.model === 'string' && + typeof entry.roi === 'number' && + typeof entry.profit_loss === 'number' && + typeof entry.win_rate === 'number' && + typeof entry.total_trades === 'number' && + typeof entry.balance === 'number' && + typeof entry.portfolio_value === 'number' && + typeof entry.total_value === 'number' + ); + setEntries(entries); + // After a leaderboard update, refresh ROI history if stale (>30s) + const now = Date.now(); + if (now - lastHistoryFetch > 30000) { + fetchROIHistory(); + } } } }, diff --git a/frontend/components/ReasoningFeed.tsx b/frontend/components/ReasoningFeed.tsx index 3924b4e..4970263 100644 --- a/frontend/components/ReasoningFeed.tsx +++ b/frontend/components/ReasoningFeed.tsx @@ -1,5 +1,6 @@ 'use client'; +import { type WebSocketMessage } from '@/lib/websocket'; import { formatDistanceToNow } from 'date-fns'; import { tr } from 'date-fns/locale'; import { Brain, Minus, TrendingDown, TrendingUp } from 'lucide-react'; @@ -19,9 +20,9 @@ interface Decision { timestamp: number; } -interface WebSocketMessage { - type: string; - data: Decision | Record; +interface AgentThinkingData { + agent_id: string; + agent_name: string; timestamp: number; } @@ -34,9 +35,9 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM if (!lastMessage) return; if (lastMessage.type === 'agent_thinking') { - const data = lastMessage.data; + const data = lastMessage.data as AgentThinkingData | Record; if (data && typeof data === 'object' && 'agent_id' in data) { - const agentId = (data as Record).agent_id; + const agentId = data.agent_id; if (typeof agentId === 'string' && agentId) { setThinkingAgents(prev => new Set(prev).add(agentId)); } @@ -44,8 +45,10 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM } if (lastMessage.type === 'agent_decision') { - const data = lastMessage.data; + const data = lastMessage.data as Decision | Record; if ( + data && + typeof data === 'object' && typeof data.agent_id === 'string' && typeof data.agent_name === 'string' && typeof data.decision_id === 'string' && diff --git a/frontend/lib/websocket.ts b/frontend/lib/websocket.ts index ff913f0..0857bff 100644 --- a/frontend/lib/websocket.ts +++ b/frontend/lib/websocket.ts @@ -4,7 +4,7 @@ import { useEffect, useRef, useState } from 'react'; export interface WebSocketMessage { type: string; - data: any; + data: unknown; timestamp: number; } From 1e796c59a1a4863165ea93683e15dfddecc2a599 Mon Sep 17 00:00:00 2001 From: 1batu <32592602+1batu@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:19:35 +0300 Subject: [PATCH 5/6] Add robust type checks for API responses in frontend Improves data validation in Dashboard, Leaderboard, and ReasoningFeed components by adding stricter type checks for API responses before updating state. This prevents potential runtime errors from malformed or unexpected data structures returned by the backend. --- frontend/components/Dashboard.tsx | 16 +++++++++- frontend/components/Leaderboard.tsx | 42 ++++++++++++++++++++++----- frontend/components/ReasoningFeed.tsx | 5 ++-- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/frontend/components/Dashboard.tsx b/frontend/components/Dashboard.tsx index 423fed9..e827c7d 100644 --- a/frontend/components/Dashboard.tsx +++ b/frontend/components/Dashboard.tsx @@ -56,7 +56,21 @@ export default function Dashboard() { useEffect(() => { fetch('http://localhost:8080/api/v1/agents') .then(res => res.json()) - .then(data => setAgents(data.data || [])); + .then((data: { success?: boolean; data?: unknown }) => { + if (data.data && Array.isArray(data.data)) { + const agents = data.data.filter((agent): agent is Agent => + agent && + typeof agent === 'object' && + typeof agent.id === 'string' && + typeof agent.name === 'string' && + typeof agent.model === 'string' && + typeof agent.current_balance === 'number' && + typeof agent.status === 'string' + ); + setAgents(agents); + } + }) + .catch(() => {}); }, []); // Initialize ref with agents when they change diff --git a/frontend/components/Leaderboard.tsx b/frontend/components/Leaderboard.tsx index 5f7607a..1b0578a 100644 --- a/frontend/components/Leaderboard.tsx +++ b/frontend/components/Leaderboard.tsx @@ -27,17 +27,24 @@ export default function Leaderboard() { const fetchROIHistory = () => { fetch('http://localhost:8080/api/v1/leaderboard/roi-history?limit=120') .then(r => r.json()) - .then(d => { - if (d.success && d.data) { + .then((d: { success?: boolean; data?: unknown }) => { + if (d.success && d.data && typeof d.data === 'object' && d.data !== null) { type RawPoint = { time: string; roi: number }; const transformed: Record = {}; - Object.entries(d.data as Record) - .forEach(([agentId, points]) => { - const sorted = points + Object.entries(d.data as Record).forEach(([agentId, points]) => { + if (Array.isArray(points)) { + const validPoints = points + .filter((p): p is RawPoint => + p && + typeof p === 'object' && + typeof (p as RawPoint).time === 'string' && + typeof (p as RawPoint).roi === 'number' + ) .map(p => ({ time: p.time, roi: p.roi })) .sort((a, b) => new Date(a.time).getTime() - new Date(b.time).getTime()); - transformed[agentId] = sorted; - }); + transformed[agentId] = validPoints; + } + }); setRoiHistory(transformed); setLastHistoryFetch(Date.now()); } @@ -78,7 +85,26 @@ export default function Leaderboard() { useEffect(() => { fetch('http://localhost:8080/api/v1/leaderboard') .then(r => r.json()) - .then(d => setEntries(d.data || [])) + .then((d: { success?: boolean; data?: unknown }) => { + if (d.data && Array.isArray(d.data)) { + const entries = d.data.filter((entry): entry is LeaderboardEntry => + entry && + typeof entry === 'object' && + typeof entry.rank === 'number' && + typeof entry.agent_id === 'string' && + typeof entry.agent_name === 'string' && + typeof entry.model === 'string' && + typeof entry.roi === 'number' && + typeof entry.profit_loss === 'number' && + typeof entry.win_rate === 'number' && + typeof entry.total_trades === 'number' && + typeof entry.balance === 'number' && + typeof entry.portfolio_value === 'number' && + typeof entry.total_value === 'number' + ); + setEntries(entries); + } + }) .catch(() => {}); fetchROIHistory(); }, []); diff --git a/frontend/components/ReasoningFeed.tsx b/frontend/components/ReasoningFeed.tsx index 4970263..bab91ac 100644 --- a/frontend/components/ReasoningFeed.tsx +++ b/frontend/components/ReasoningFeed.tsx @@ -35,9 +35,10 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM if (!lastMessage) return; if (lastMessage.type === 'agent_thinking') { - const data = lastMessage.data as AgentThinkingData | Record; + const data = lastMessage.data; if (data && typeof data === 'object' && 'agent_id' in data) { - const agentId = data.agent_id; + const typedData = data as Record; + const agentId = typedData.agent_id; if (typeof agentId === 'string' && agentId) { setThinkingAgents(prev => new Set(prev).add(agentId)); } From 3bcd58a506c7c7977d5f9300bae4aa9600dc8639 Mon Sep 17 00:00:00 2001 From: 1batu <32592602+1batu@users.noreply.github.com> Date: Sun, 9 Nov 2025 21:26:31 +0300 Subject: [PATCH 6/6] Update ReasoningFeed.tsx --- frontend/components/ReasoningFeed.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/frontend/components/ReasoningFeed.tsx b/frontend/components/ReasoningFeed.tsx index bab91ac..1eff9bc 100644 --- a/frontend/components/ReasoningFeed.tsx +++ b/frontend/components/ReasoningFeed.tsx @@ -36,10 +36,11 @@ export default function ReasoningFeed({ lastMessage }: { lastMessage: WebSocketM if (lastMessage.type === 'agent_thinking') { const data = lastMessage.data; - if (data && typeof data === 'object' && 'agent_id' in data) { + if (data && typeof data === 'object' && data !== null && 'agent_id' in data) { const typedData = data as Record; - const agentId = typedData.agent_id; - if (typeof agentId === 'string' && agentId) { + const agentIdValue = typedData.agent_id; + if (typeof agentIdValue === 'string' && agentIdValue.length > 0) { + const agentId: string = agentIdValue; setThinkingAgents(prev => new Set(prev).add(agentId)); } }